From fb7d76a2aba2c5aaa2d7415adc8c90fd510d6ef1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 24 Jul 2025 23:50:27 -0400 Subject: [PATCH 01/12] working --- packages/core/controls.mjs | 12 ++++ packages/superdough/helpers.mjs | 17 +++++- packages/superdough/noise.mjs | 2 +- packages/superdough/synth.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 69 +++++++++++++++++++++++ 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 326343e10..af0136e54 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -252,6 +252,18 @@ export const { fmenv } = registerControl('fmenv'); * */ export const { fmattack } = registerControl('fmattack'); + +/** + * Attack time for the FM envelope: time it takes to reach maximum modulation + * + * @name fmwave + * @param {number | Pattern} wave waveform + * @example + * n("0 1 2 3".fast(4)).chord("").voicing().s("sawtooth").fmwave("brown").fm(.6) + * + */ +export const { fmwave } = registerControl('fmwave'); + /** * Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase. * diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 6bde69373..63643ebbc 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,5 +1,8 @@ import { getAudioContext } from './superdough.mjs'; import { clamp, nanFallback } from './util.mjs'; +import { getNoiseBuffer } from './noise.mjs'; + +export const noises = ['pink', 'white', 'brown', 'crackle']; export function gainNode(value) { const node = getAudioContext().createGain(); @@ -216,9 +219,17 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { } const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); - const osc = ctx.createOscillator(); - osc.type = type; - osc.frequency.value = freq; + let osc; + if (noises.includes(type)) { + osc = ctx.createBufferSource(); + osc.buffer = getNoiseBuffer(type, 2); + osc.loop = true; + } else { + osc = ctx.createOscillator(); + osc.type = type; + osc.frequency.value = freq; + } + osc.start(); const g = new GainNode(ctx, { gain: range }); osc.connect(g); // -range, range diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 247794702..5411f2f2f 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs'; let noiseCache = {}; // lazy generates noise buffers and keeps them forever -function getNoiseBuffer(type, density) { +export function getNoiseBuffer(type, density) { const ac = getAudioContext(); if (noiseCache[type]) { return noiseCache[type]; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 88e14e5ab..255e0342b 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -9,6 +9,7 @@ import { getVibratoOscillator, webAudioTimeout, getWorklet, + noises, } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; @@ -40,7 +41,6 @@ const waveformAliases = [ ['saw', 'sawtooth'], ['sin', 'sine'], ]; -const noises = ['pink', 'white', 'brown', 'crackle']; export function registerSynthSounds() { [...waveforms].forEach((s) => { diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e6bfa44aa..2725e1d99 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3706,6 +3706,75 @@ exports[`runs examples > example "fmsustain" example index 0 1`] = ` ] `; +exports[`runs examples > example "fmwave" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/16 → 1/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/8 → 3/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/16 → 1/4 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/4 → 5/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/16 → 3/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/8 → 7/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/16 → 1/2 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/2 → 9/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 9/16 → 5/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/8 → 11/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 11/16 → 3/4 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/4 → 13/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 13/16 → 7/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/8 → 15/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 15/16 → 1/1 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/1 → 17/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 17/16 → 9/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 9/8 → 19/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 19/16 → 5/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/4 → 21/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 21/16 → 11/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 11/8 → 23/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 23/16 → 3/2 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/2 → 25/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 25/16 → 13/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 13/8 → 27/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 27/16 → 7/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/4 → 29/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 29/16 → 15/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 15/8 → 31/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 31/16 → 2/1 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 2/1 → 33/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 33/16 → 17/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 17/8 → 35/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 35/16 → 9/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 9/4 → 37/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 37/16 → 19/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 19/8 → 39/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 39/16 → 5/2 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/2 → 41/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 41/16 → 21/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 21/8 → 43/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 43/16 → 11/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 11/4 → 45/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 45/16 → 23/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 23/8 → 47/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 47/16 → 3/1 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/1 → 49/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 49/16 → 25/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 25/8 → 51/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 51/16 → 13/4 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 13/4 → 53/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 53/16 → 27/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 27/8 → 55/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 55/16 → 7/2 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/2 → 57/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 57/16 → 29/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 29/8 → 59/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 59/16 → 15/4 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 15/4 → 61/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 61/16 → 31/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 31/8 → 63/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 63/16 → 4/1 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", +] +`; + exports[`runs examples > example "focus" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:sd ]", From 82f0dbb750c40ab04197b6d27666341fa59a0bc2 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 24 Jul 2025 23:59:38 -0400 Subject: [PATCH 02/12] add another example --- packages/core/controls.mjs | 4 +- test/__snapshots__/examples.test.mjs.snap | 69 +++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index af0136e54..24c75970c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -254,11 +254,13 @@ export const { fmenv } = registerControl('fmenv'); export const { fmattack } = registerControl('fmattack'); /** - * Attack time for the FM envelope: time it takes to reach maximum modulation + * waveform of the modulator * * @name fmwave * @param {number | Pattern} wave waveform * @example + * n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("").fm(4).fmh(2.01) + * @example * n("0 1 2 3".fast(4)).chord("").voicing().s("sawtooth").fmwave("brown").fm(.6) * */ diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 2725e1d99..eb6c238d5 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3707,6 +3707,75 @@ exports[`runs examples > example "fmsustain" example index 0 1`] = ` `; exports[`runs examples > example "fmwave" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/16 → 1/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/8 → 3/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 3/16 → 1/4 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/4 → 5/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 5/16 → 3/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 3/8 → 7/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 7/16 → 1/2 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/2 → 9/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 9/16 → 5/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 5/8 → 11/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 11/16 → 3/4 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 3/4 → 13/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 13/16 → 7/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 7/8 → 15/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 15/16 → 1/1 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/1 → 17/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 17/16 → 9/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 9/8 → 19/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 19/16 → 5/4 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 5/4 → 21/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 21/16 → 11/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 11/8 → 23/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 23/16 → 3/2 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 3/2 → 25/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 25/16 → 13/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 13/8 → 27/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 27/16 → 7/4 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 7/4 → 29/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 29/16 → 15/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 15/8 → 31/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 31/16 → 2/1 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 2/1 → 33/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 33/16 → 17/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 17/8 → 35/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 35/16 → 9/4 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 9/4 → 37/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 37/16 → 19/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 19/8 → 39/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 39/16 → 5/2 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 5/2 → 41/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 41/16 → 21/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 21/8 → 43/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 43/16 → 11/4 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 11/4 → 45/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 45/16 → 23/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 23/8 → 47/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 47/16 → 3/1 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 3/1 → 49/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 49/16 → 25/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 25/8 → 51/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 51/16 → 13/4 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 13/4 → 53/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 53/16 → 27/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 27/8 → 55/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 55/16 → 7/2 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 7/2 → 57/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 57/16 → 29/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 29/8 → 59/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 59/16 → 15/4 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 15/4 → 61/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 61/16 → 31/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 31/8 → 63/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 63/16 → 4/1 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", +] +`; + +exports[`runs examples > example "fmwave" example index 1 1`] = ` [ "[ 0/1 → 1/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", "[ 1/16 → 1/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", From dae1560c409dab31828ecfe0e851348a524ed76b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 25 Jul 2025 00:00:23 -0400 Subject: [PATCH 03/12] description --- packages/core/controls.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 24c75970c..7071387af 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -254,7 +254,7 @@ export const { fmenv } = registerControl('fmenv'); export const { fmattack } = registerControl('fmattack'); /** - * waveform of the modulator + * waveform of the fm modulator * * @name fmwave * @param {number | Pattern} wave waveform From 3b6e3624be36dc66482c03be0165215628b37cd0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 27 Jul 2025 01:07:45 -0400 Subject: [PATCH 04/12] working --- packages/superdough/synth.mjs | 79 ++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 88e14e5ab..34f958f11 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -12,9 +12,9 @@ import { } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -const getFrequencyFromValue = (value) => { +const getFrequencyFromValue = (value, defaultNote = 36) => { let { note, freq } = value; - note = note || 36; + note = note || defaultNote; if (typeof note === 'string') { note = noteToMidi(note); // e.g. c3 => 48 } @@ -42,6 +42,19 @@ const waveformAliases = [ ]; const noises = ['pink', 'white', 'brown', 'crackle']; +function makeDistortionCurve(amount) { + const k = typeof amount === 'number' ? amount : 50; + const n_samples = 44100; + const curve = new Float32Array(n_samples); + const deg = Math.PI / 180; + + for (let i = 0; i < n_samples; i++) { + const x = (i * 2) / n_samples - 1; + curve[i] = Math.tanh(x * k); + } + return curve; +} + export function registerSynthSounds() { [...waveforms].forEach((s) => { registerSound( @@ -84,6 +97,68 @@ export function registerSynthSounds() { { type: 'synth', prebake: true }, ); }); + + registerSound( + '909bd', + (t, value, onended) => { + const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value; + const ctx = getAudioContext(); + const attackhold = 0.02; + const noiselvl = 1.2; + const noisedecay = 0.025; + + const o = ctx.createOscillator(); + o.type = 'triangle'; + o.frequency.value = getFrequencyFromValue(value, 29); + o.detune.setValueAtTime(penv * 100, 0); + o.detune.setValueAtTime(penv * 100, t); + o.detune.exponentialRampToValueAtTime(0.001, t + pdecay); + const g = gainNode(1); + g.gain.setValueAtTime(1, t + attackhold); + g.gain.exponentialRampToValueAtTime(0.001, t + attackhold + decay); + o.start(t); + + const noise = getNoiseOscillator('brown', t, 2); + const noiseGain = gainNode(1); + noiseGain.gain.setValueAtTime(noiselvl, t); + noiseGain.gain.exponentialRampToValueAtTime(0.001, t + noisedecay); + + const sat = new WaveShaperNode(ctx); + // tri to sine diode shaper emulation + sat.curve = makeDistortionCurve(2); + + const mix = gainNode(1); + o.onended = () => { + o.disconnect(); + g.disconnect(); + sat.disconnect(); + noise.node.disconnect(); + noiseGain.disconnect(); + mix.disconnect(); + onended(); + }; + + const node = o.connect(sat).connect(g).connect(mix); + noise.node.connect(noiseGain).connect(mix); + const holdEnd = t + decay; + + let end = holdEnd + 0.01; + if (clip != null) { + end = Math.min(t + clip * duration, end); + } + o.stop(end); + noise.stop(end); + + return { + node, + stop: (endTime) => { + o.stop(endTime); + }, + }; + }, + { type: 'synth', prebake: true }, + ); + registerSound( 'supersaw', (begin, value, onended) => { From 7691403fd08f23de09604e1e277619de6db4f148 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 27 Jul 2025 01:22:59 -0400 Subject: [PATCH 05/12] prevent clicks --- packages/superdough/synth.mjs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 34f958f11..e72e669eb 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -106,6 +106,7 @@ export function registerSynthSounds() { const attackhold = 0.02; const noiselvl = 1.2; const noisedecay = 0.025; + const mixGain = 1; const o = ctx.createOscillator(); o.type = 'triangle'; @@ -127,7 +128,8 @@ export function registerSynthSounds() { // tri to sine diode shaper emulation sat.curve = makeDistortionCurve(2); - const mix = gainNode(1); + const mix = gainNode(mixGain); + o.onended = () => { o.disconnect(); g.disconnect(); @@ -140,12 +142,17 @@ export function registerSynthSounds() { const node = o.connect(sat).connect(g).connect(mix); noise.node.connect(noiseGain).connect(mix); - const holdEnd = t + decay; + const holdEnd = t + decay; let end = holdEnd + 0.01; if (clip != null) { end = Math.min(t + clip * duration, end); } + + // prevent clicking + mix.gain.setValueAtTime(mixGain, end - 0.01); + mix.gain.linearRampToValueAtTime(0, end); + o.stop(end); noise.stop(end); From 4d13b981f16db4c281505708f8cfe09e904efe09 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 27 Jul 2025 01:28:50 -0400 Subject: [PATCH 06/12] variable srate --- packages/superdough/synth.mjs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index e72e669eb..8f4686a80 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -42,11 +42,9 @@ const waveformAliases = [ ]; const noises = ['pink', 'white', 'brown', 'crackle']; -function makeDistortionCurve(amount) { +function makeSaturationCurve(amount, n_samples) { const k = typeof amount === 'number' ? amount : 50; - const n_samples = 44100; const curve = new Float32Array(n_samples); - const deg = Math.PI / 180; for (let i = 0; i < n_samples; i++) { const x = (i * 2) / n_samples - 1; @@ -126,7 +124,7 @@ export function registerSynthSounds() { const sat = new WaveShaperNode(ctx); // tri to sine diode shaper emulation - sat.curve = makeDistortionCurve(2); + sat.curve = makeSaturationCurve(2, ctx.sampleRate); const mix = gainNode(mixGain); From 973ae4c7da4a036ea9147d61377c62f4513e5c28 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 27 Jul 2025 15:02:00 -0400 Subject: [PATCH 07/12] change 909bd to sbd --- packages/superdough/synth.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 8f4686a80..9446103f9 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -97,7 +97,7 @@ export function registerSynthSounds() { }); registerSound( - '909bd', + 'sbd', (t, value, onended) => { const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value; const ctx = getAudioContext(); From 9017085fa301a549889b89412dba161315a6821b Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Fri, 1 Aug 2025 09:36:36 +0100 Subject: [PATCH 08/12] add trans alias for transpose --- packages/tonal/tonal.mjs | 73 +++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index a425782d2..5ac894f56 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -61,40 +61,7 @@ function scaleOffset(scale, offset, note) { return n + o; } -// Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { -/** - * Change the pitch of each value by the given amount. Expects numbers or note strings as values. - * The amount can be given as a number of semitones or as a string in interval short notation. - * If you don't care about enharmonic correctness, just use numbers. Otherwise, pass the interval of - * the form: ST where S is the degree number and T the type of interval with - * - * - M = major - * - m = minor - * - P = perfect - * - A = augmented - * - d = diminished - * - * Examples intervals: - * - * - 1P = unison - * - 3M = major third - * - 3m = minor third - * - 4P = perfect fourth - * - 4A = augmented fourth - * - 5P = perfect fifth - * - 5d = diminished fifth - * - * @param {string | number} amount Either number of semitones or interval string. - * @returns Pattern - * @memberof Pattern - * @name transpose - * @example - * "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note() - * @example - * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note() - */ - -export const transpose = register('transpose', function (intervalOrSemitones, pat) { +function transposeFn(intervalOrSemitones, pat) { return pat.withHap((hap) => { const note = hap.value.note ?? hap.value; if (typeof note === 'number') { @@ -128,7 +95,43 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa } return hap.withValue(() => targetNote); }); -}); +} + +// Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { +/** + * Change the pitch of each value by the given amount. Expects numbers or note strings as values. + * The amount can be given as a number of semitones or as a string in interval short notation. + * If you don't care about enharmonic correctness, just use numbers. Otherwise, pass the interval of + * the form: ST where S is the degree number and T the type of interval with + * + * - M = major + * - m = minor + * - P = perfect + * - A = augmented + * - d = diminished + * + * Examples intervals: + * + * - 1P = unison + * - 3M = major third + * - 3m = minor third + * - 4P = perfect fourth + * - 4A = augmented fourth + * - 5P = perfect fifth + * - 5d = diminished fifth + * + * @param {string | number} amount Either number of semitones or interval string. + * @returns Pattern + * @memberof Pattern + * @name transpose + * @example + * "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note() + * @example + * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note() + */ + +export const transpose = register('transpose', transposeFn); +export const trans = register('trans', transposeFn); // example: transpose(3).late(0.2) will be equivalent to compose(transpose(3), late(0.2)) // e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or From ef3cd56b0dca8f6f677f8b1e72386c2763464fa6 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Fri, 1 Aug 2025 09:39:21 +0100 Subject: [PATCH 09/12] add synonym to docs --- packages/tonal/tonal.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 5ac894f56..da3a4d507 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -124,6 +124,7 @@ function transposeFn(intervalOrSemitones, pat) { * @returns Pattern * @memberof Pattern * @name transpose + * @synonyms trans * @example * "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note() * @example From 051efdc13bdd184daf328fcd146eaf109ea97243 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Fri, 1 Aug 2025 09:39:33 +0100 Subject: [PATCH 10/12] flyby improvement: add dec synonym to docs --- packages/core/controls.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 326343e10..6b3433c31 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -306,6 +306,7 @@ export const { fft } = registerControl('fft'); * * @name decay * @param {number | Pattern} time decay time in seconds + * @synonyms dec * @example * note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0) * From 39c45bc2b2077a2b7cdcd6365af85c2cc15e8395 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Sat, 2 Aug 2025 10:17:33 +0100 Subject: [PATCH 11/12] pass an array of names instead --- packages/tonal/tonal.mjs | 73 +++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index da3a4d507..7cb987394 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -61,42 +61,6 @@ function scaleOffset(scale, offset, note) { return n + o; } -function transposeFn(intervalOrSemitones, pat) { - return pat.withHap((hap) => { - const note = hap.value.note ?? hap.value; - if (typeof note === 'number') { - // note is a number, so just add the number semitones of the interval - let semitones; - if (typeof intervalOrSemitones === 'number') { - semitones = intervalOrSemitones; - } else if (typeof intervalOrSemitones === 'string') { - semitones = Interval.semitones(intervalOrSemitones) || 0; - } - const targetNote = note + semitones; - if (typeof hap.value === 'object') { - return hap.withValue(() => ({ ...hap.value, note: targetNote })); - } - return hap.withValue(() => targetNote); - } - if (typeof note !== 'string' || !isNote(note)) { - logger(`[tonal] transpose: not a note "${note}"`, 'warning'); - return hap; - } - // note is a string, so we might be able to preserve harmonics if interval is a string as well - const interval = !isNaN(Number(intervalOrSemitones)) - ? Interval.fromSemitones(intervalOrSemitones) - : String(intervalOrSemitones); - // TODO: move simplify to player to preserve enharmonics - // tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3 - // TODO: check if this is still relevant.. - const targetNote = Note.simplify(Note.transpose(note, interval)); - if (typeof hap.value === 'object') { - return hap.withValue(() => ({ ...hap.value, note: targetNote })); - } - return hap.withValue(() => targetNote); - }); -} - // Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { /** * Change the pitch of each value by the given amount. Expects numbers or note strings as values. @@ -131,8 +95,41 @@ function transposeFn(intervalOrSemitones, pat) { * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note() */ -export const transpose = register('transpose', transposeFn); -export const trans = register('trans', transposeFn); +export const transpose = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) { + return pat.withHap((hap) => { + const note = hap.value.note ?? hap.value; + if (typeof note === 'number') { + // note is a number, so just add the number semitones of the interval + let semitones; + if (typeof intervalOrSemitones === 'number') { + semitones = intervalOrSemitones; + } else if (typeof intervalOrSemitones === 'string') { + semitones = Interval.semitones(intervalOrSemitones) || 0; + } + const targetNote = note + semitones; + if (typeof hap.value === 'object') { + return hap.withValue(() => ({ ...hap.value, note: targetNote })); + } + return hap.withValue(() => targetNote); + } + if (typeof note !== 'string' || !isNote(note)) { + logger(`[tonal] transpose: not a note "${note}"`, 'warning'); + return hap; + } + // note is a string, so we might be able to preserve harmonics if interval is a string as well + const interval = !isNaN(Number(intervalOrSemitones)) + ? Interval.fromSemitones(intervalOrSemitones) + : String(intervalOrSemitones); + // TODO: move simplify to player to preserve enharmonics + // tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3 + // TODO: check if this is still relevant.. + const targetNote = Note.simplify(Note.transpose(note, interval)); + if (typeof hap.value === 'object') { + return hap.withValue(() => ({ ...hap.value, note: targetNote })); + } + return hap.withValue(() => targetNote); + }); +}); // example: transpose(3).late(0.2) will be equivalent to compose(transpose(3), late(0.2)) // e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or From ad113b3888888fbc34b1742219fab93dadcd4b8d Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Sat, 2 Aug 2025 10:17:48 +0100 Subject: [PATCH 12/12] document ability to specify an array of names --- packages/core/pattern.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d59a1285d..4d0274c7a 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1569,7 +1569,7 @@ export const func = curry((a, b) => reify(b).func(a)); /** * Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register. * - * @param {string} name name of the function + * @param {string | string[]} name name of the function, or an array of names to be used as synonyms * @param {function} func function with 1 or more params, where last is the current pattern * @noAutocomplete *