diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 06b2ab49..ce68cb33 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -18,6 +18,9 @@ export const soundMap = map(); export function setSound(key, onTrigger, data = {}) { soundMap.setKey(key, { onTrigger, data }); } +export function getSound(s) { + return soundMap.get()[s]; +} export const resetLoadedSounds = () => soundMap.set({}); let audioContext; @@ -166,8 +169,8 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => { let sourceNode; if (source) { sourceNode = source(t, hap.value); - } else if (soundMap.get()[s]) { - const { onTrigger } = soundMap.get()[s]; + } else if (getSound(s)) { + const { onTrigger } = getSound(s); const soundHandle = await onTrigger(t, hap.value, onended); if (soundHandle) { sourceNode = soundHandle.node; diff --git a/website/src/config.ts b/website/src/config.ts index 266d60c0..bf26fff1 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -73,6 +73,7 @@ export const SIDEBAR: Sidebar = { ], Development: [ { text: 'REPL', link: 'technical-manual/repl' }, + { text: 'Sounds', link: 'technical-manual/sounds' }, { text: 'Packages', link: 'technical-manual/packages' }, { text: 'Docs', link: 'technical-manual/docs' }, { text: 'Testing', link: 'technical-manual/testing' }, diff --git a/website/src/pages/technical-manual/sounds.mdx b/website/src/pages/technical-manual/sounds.mdx new file mode 100644 index 00000000..b2f81ca7 --- /dev/null +++ b/website/src/pages/technical-manual/sounds.mdx @@ -0,0 +1,75 @@ +--- +title: Sounds +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; + +# Sounds + +Let's take a closer look about how sounds are implemented in the webaudio output. + +## Registering a sound via setSound + +All sounds are registered in the sound map, using the the `setSound` function: + +```ts +function setSound( + name: string, // The name of the sound that should be given to `s`, e.g. `mysaw` + // The function called by the scheduler to trigger the sound: + ( + time: number, // The audio context time the sound should start + value: object, // The value of the `Hap` + onended: () => void // A callback that should be fired when the sound has ended + ) => { + node: AudioNode, // node to connect to rest of the effects chain + stop: (time:number) => void // a function that will stop the sound + }, + data: object // meta data, only for ui logic in sounds tab +); +``` + +When `setSound` is called, it registers `{ onTrigger, data }` under the given `name` in a [nanostore map](https://github.com/nanostores/nanostores#maps). + +### Example + +This might be a bit abstract, so here is a minimal example: + +```js +setSound( + 'mysaw', + (time, value, onended) => { + let { freq } = value; // destructure control params + const ctx = getAudioContext(); + // create oscillator + const o = new OscillatorNode(ctx, { type: 'sawtooth', frequency: Number(freq) }); + o.start(time); + // add gain node to level down osc + const g = new GainNode(ctx, { gain: 0.3 }); + // connect osc to gain + const node = o.connect(g); + // this function can be called from outside to stop the sound + const stop = (time) => o.stop(time); + // ended will be fired when stop has been fired + o.addEventListener('ended', () => { + o.disconnect(); + g.disconnect(); + onended(); + }); + return { node, stop }; + }, + { type: 'synth' }, +); +// use the sound +freq(220, 440, 330).s('mysaw'); +``` + +You can actually use this code in the [REPL](https://strudel.tidalcycles.org/) and it'll work. +After evaluating the code, you should see `mysaw` in listed in the sounds tab. + +## Playing sounds + +Now here is what happens when a sound is played: +When the webaudio output plays a `Hap`, it will lookup and call the `onTrigger` function for the given `s`. +The returned `node` can then be connected to the rest of the standard effects chain +Having the stop function separate allows playing sounds via midi too, where you don't know how long the noteon will last