Svelte + Elementary Audio + Vite
The mission continues: find the best developer experience for creating simple audio applications. I've been spending tons of time with Javascript the last year, so I want to stick with Elementary Audio for a bit. I had been using React with Elementary Audio, and, I don't know if it was the HMR (obviously not React's fault...) or just the extra re-renders that React/useEffect does is development but I was getting results that sounded like AudioContexts stacking up on each other (aka heavily distorted). I spent a bit of time doing the React thing => trying to isolate components, useCallback'ing functions, cleaning up useEffects, etc but it felt like the application logic was simply getting unnecessarily complicated (and I wasn't even at the complex part of the application yet!).
So Svelte. I hear it's fast and light. People rave about the developer experience. I decided to make a proof of concept app with Elementary Audio to see if my time might better spent with Svelte as my tool for developing Elementary Audio web apps.
The results are promising. Audio quality and responsiveness is up to par and the application structure is really quite simple.
I'm using yarn create vite
to bootstrap this thing.
Initialize the main components of the app (AudioContext, WebRenderer) and then make them available throughout the app in a store.
// in lib/stores.js
import { writable } from "svelte/store";
import WebRenderer from "@elemaudio/web-renderer";
let context = new AudioContext();
let renderer = new WebRenderer();
(async () => {
let node = await renderer.initialize(context, {
numberOfInputs: 0,
numberOfOutputs: 1,
outputChannelCount: [2],
});
node.connect(context.destination);
})();
export const core = writable(renderer);
export const ctx = writable(context);
And then I have one component handling the AudioContext play/pause functionality and one component handling the audio engine rendering.
<script>
import { ctx } from "./stores.js"
const togglePlay = async () => {
if ($ctx.state === 'suspended') {
await $ctx.resume();
} else {
await $ctx.suspend();
}
}
</script>
<button on:click={togglePlay}>Play</button>
<script>
import { el } from "@elemaudio/core";
import { core } from "./stores.js"
import { noteToHz } from "./utils.js";
import * as lib from "./elem-components.js";
let amp = 0.1;
let freq = 3;
let delayTime = 525;
let delayFb = 0.1;
let steps = [1, 5, 7, 8, 10];
let attack = 0.2;
let decay = 0.65;
//
// rerun render on any param change
const renderChanges = () => {
let kAmp = el.const({ key: "amp", value: amp });
let kFreq = el.const({ key: "freq", value: freq });
let kTime = el.const({ key: "time", value: delayTime });
let kFb = el.const({ key: "fb", value: delayFb });
let kAtk = el.const({ key: "attack", value: attack });
let kDecay = el.const({ key: "decay", value: decay });
let arp = steps.map(x => noteToHz(x))
let env = el.adsr(kAtk, kDecay, 0, 0, el.train(kFreq));
let dry = el.mul(kAmp, env, el.cycle(el.seq2({ seq: arp }, el.train(kFreq), 0)));
let delay = lib.delay(dry, kTime, kFb);
let sum = el.add(dry, delay);
$core.render(el.dcblock(sum), el.dcblock(sum))
}
$core.on("load", () => {
renderChanges();
})
</script>
<form class="flex-col" on:change={renderChanges}>
<label class="flex-col">
amplitude: {amp}
<input type="range" min="0" max="1" step="0.1" bind:value={amp}>
</label>
<label class="flex-col">
attack: {attack}
<input type="range" min="0" max="3" step="0.1" bind:value={attack}>
</label>
<label class="flex-col">
decay: {decay}
<input type="range" min="0.1" max="3" step="0.1" bind:value={decay}>
</label>
<label class="flex-col">
seq speed: {freq}
<input type="range" min="1" max="20" step="0.1" bind:value={freq}>
</label>
<label class="flex-col">
5 step seq: {steps}
{#each steps as step, i}
<input type="range" min="0" max="24" step="1" bind:value={steps[i]}>
{/each}
</label>
<label class="flex-col">
delay time: {delayTime} ms
<input type="range" min="30" max="1200" step="1" bind:value={delayTime}>
</label>
<label class="flex-col">
delay feedback: {delayFb}
<input type="range" min="0" max="1" step="0.1" bind:value={delayFb}>
</label>
</form>