From 2f2bf5bf29541ea1c4edee5e70302b109f54ce0b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 7 Jun 2025 22:36:54 +0200 Subject: [PATCH 1/7] fix: note2midi for notes without octave --- packages/supradough/dough.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 08469e63..2853f397 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -457,7 +457,7 @@ let getDefaultValue = (key) => defaultDefaultValues[key]; const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }; const accs = { '#': 1, b: -1, s: 1, f: -1 }; const note2midi = (note, defaultOctave = 3) => { - const [pc, acc = '', oct = defaultOctave] = + let [pc, acc = '', oct = ''] = String(note) .match(/^([a-gA-G])([#bsf]*)([0-9]*)$/) ?.slice(1) || []; @@ -466,13 +466,14 @@ const note2midi = (note, defaultOctave = 3) => { } const chroma = chromas[pc.toLowerCase()]; const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0; - return (Number(oct) + 1) * 12 + chroma + offset; + oct = Number(oct || defaultOctave); + return (oct + 1) * 12 + chroma + offset; }; const getFrequency = (value) => { let { note, freq } = value; note = note || 36; if (typeof note === 'string') { - note = note2midi(note); // e.g. c3 => 48 + note = note2midi(note, 3); // e.g. c3 => 48 } if (!freq && typeof note === 'number') { freq = Math.pow(2, (note - 69) / 12) * 440; From 5988e3d3b48541444440f6c165ea1b37ed22ff02 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 7 Jun 2025 22:42:48 +0200 Subject: [PATCH 2/7] fix: hot reloading for dough.mjs (see comment) --- packages/supradough/index.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/supradough/index.mjs b/packages/supradough/index.mjs index 1bce17f6..54a83549 100644 --- a/packages/supradough/index.mjs +++ b/packages/supradough/index.mjs @@ -1,4 +1,4 @@ -import _workletUrl from './dough-worklet.mjs?audioworklet'; +import _workletUrl from './dough-worklet.mjs?url'; // todo: change ?url to ?audioworklet before build (?audioworklet doesn't hot reload) export * from './dough.mjs'; export const workletUrl = _workletUrl; From ab052ce1ac6fe4232edd65e877dc2cb041df0d48 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 7 Jun 2025 23:15:09 +0200 Subject: [PATCH 3/7] fix: polyblepped sawtooth + fm = death --- packages/supradough/dough.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 2853f397..826172a2 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -450,6 +450,7 @@ const defaultDefaultValues = { z: 'triangle', pan: 0.5, fmh: 1, + fmenv: 0, // differs from superdough }; let getDefaultValue = (key) => defaultDefaultValues[key]; @@ -540,6 +541,7 @@ export class DoughVoice { this.fft = this.fft ?? getDefaultValue('fft'); this.pan = this.pan ?? getDefaultValue('pan'); this.orbit = this.orbit ?? getDefaultValue('orbit'); + this.fmenv = this.fmenv ?? getDefaultValue('fmenv'); [this.attack, this.decay, this.sustain, this.release] = getADSRValues([ this.attack, @@ -551,6 +553,9 @@ export class DoughVoice { this._holdEnd = this._begin + this._duration; // needed for gate this._end = this._holdEnd + this.release + 0.01; // needed for despawn + if (this.s === 'saw' || this.s === 'sawtooth') { + this.s = 'zaw'; // polyblepped saw when fm is applied + } const SourceClass = oscillators[this.s] ?? TriOsc; this._sound = new SourceClass(); @@ -638,7 +643,7 @@ export class DoughVoice { let fmi = this.fmi; if (this._fmenv) { const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease) ** 2; - fmi = /* 2 ** */ this.fmenv * env * fmi; // todo: find good scaling + fmi = this.fmenv * env * fmi; } const modfreq = freq * this.fmh; const modgain = modfreq * fmi; From ac7ee376e5a0ed121526096baa3839a2e117aebe Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 8 Jun 2025 00:43:58 +0200 Subject: [PATCH 4/7] pitch envelope --- packages/supradough/dough.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 826172a2..809b8d52 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -559,6 +559,16 @@ export class DoughVoice { const SourceClass = oscillators[this.s] ?? TriOsc; this._sound = new SourceClass(); + if (this.penv) { + this._penv = new ADSR(); + [this.pattack, this.pdecay, this.psustain, this.prelease] = getADSRValues([ + this.pattack, + this.pdecay, + this.psustain, + this.prelease, + ]); + } + if (this.fmi) { this._fm = new SineOsc(); this.fmh = this.fmh ?? getDefaultValue('fmh'); @@ -650,6 +660,11 @@ export class DoughVoice { freq = freq + this._fm.update(modfreq) * modgain; } + if (this._penv) { + const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease) ** 2; + freq = freq + env * this.penv; + } + // sound source if (this.s === 'pulse') { s = this._sound.update(freq, this.pw ?? 0.5); From 0f2aa9569b9cabc3c1144e01c5586e4099baab0d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 8 Jun 2025 02:04:49 +0200 Subject: [PATCH 5/7] half working samples --- packages/supradough/dough-worklet.mjs | 14 ++- packages/supradough/dough.mjs | 41 ++++++-- packages/webaudio/index.mjs | 1 + packages/webaudio/supradough.mjs | 138 +++++++++++++++++++++++++- 4 files changed, 182 insertions(+), 12 deletions(-) diff --git a/packages/supradough/dough-worklet.mjs b/packages/supradough/dough-worklet.mjs index 0361afa0..97a35c48 100644 --- a/packages/supradough/dough-worklet.mjs +++ b/packages/supradough/dough-worklet.mjs @@ -6,7 +6,19 @@ class DoughProcessor extends AudioWorkletProcessor { constructor() { super(); this.dough = new Dough(sampleRate, currentTime); - this.port.onmessage = (event) => this.dough.scheduleSpawn(event.data); + this.port.onmessage = (event) => { + if (event.data.spawn) { + this.dough.scheduleSpawn(event.data.spawn); + } else if (event.data.sample) { + this.dough.loadSample(event.data.sample, event.data.channels); + } else if (event.data.samples) { + event.data.samples.forEach(([name, channels]) => { + this.dough.loadSample(name, channels); + }); + } else { + console.log('unrecognized event type', event.data); + } + }; } process(inputs, outputs, params) { if (this.disconnected) { diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 809b8d52..e42b4f8e 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -383,6 +383,19 @@ export class Distort { } // distortion could be expressed as a function, because it's stateless +export class BufferPlayer { + channels = []; + pos = 0; + update(freq, channel = 0) { + if (this.pos >= this.channels[channel].length) { + return 0; + } + let s = this.channels[channel][this.pos]; + this.pos++; + return s; + } +} + export function _rangex(sig, min, max) { let logmin = Math.log(min); let range = Math.log(max) - logmin; @@ -403,7 +416,7 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => { return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; }; -let oscillators = { +let shapes = { sine: SineOsc, saw: SawOsc, zaw: ZawOsc, @@ -553,11 +566,20 @@ export class DoughVoice { this._holdEnd = this._begin + this._duration; // needed for gate this._end = this._holdEnd + this.release + 0.01; // needed for despawn + this.s ??= 'triangle'; if (this.s === 'saw' || this.s === 'sawtooth') { this.s = 'zaw'; // polyblepped saw when fm is applied } - const SourceClass = oscillators[this.s] ?? TriOsc; - this._sound = new SourceClass(); + if (shapes[this.s]) { + const SourceClass = shapes[this.s]; + this._sound = new SourceClass(); + } else if (value.samples.has(this.s)) { + this._sample = new BufferPlayer(); + this._sample.channels = value.samples.get(this.s); + this._sample.pos = 0; + } else { + console.warn('sound not found', this.s); + } if (this.penv) { this._penv = new ADSR(); @@ -642,7 +664,7 @@ export class DoughVoice { return 1 - Math.log(c) * this.eighthOverLogHalf; } update(t) { - if (!this._sound) { + if (!this._sound && !this._sample) { return 0; } let s = 0; @@ -666,10 +688,12 @@ export class DoughVoice { } // sound source - if (this.s === 'pulse') { + if (this._sound && this.s === 'pulse') { s = this._sound.update(freq, this.pw ?? 0.5); - } else { + } else if (this._sound) { s = this._sound.update(freq); + } else if (this._sample) { + s = this._sample.update(freq, 0); // tbd: stereo samples... } s = s * this.gain * this.velocity; @@ -738,6 +762,7 @@ export class Dough { delaysend = [0, 0]; delaytime = getDefaultValue('delaytime'); delayfeedback = getDefaultValue('delayfeedback'); + samples = new Map(); t = 0; // sampleRate: number, currentTime: number (seconds) constructor(sampleRate = 48000, currentTime = 0) { @@ -747,6 +772,9 @@ export class Dough { this._delayL = new Delay(); this._delayR = new Delay(); } + loadSample(name, channels) { + this.samples.set(name, channels); + } scheduleSpawn(value) { if (value._begin === undefined) { throw new Error('[dough]: scheduleSpawn expected _begin to be set'); @@ -760,6 +788,7 @@ export class Dough { } spawn(value) { value.id = this.vid++; + value.samples = this.samples; const voice = new DoughVoice(value); this.voices.push(voice); // console.log('spawn', voice.id, 'voices:', this.voices.length); diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index 59672b61..f89e1269 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -7,4 +7,5 @@ This program is free software: you can redistribute it and/or modify it under th export * from './webaudio.mjs'; export * from './scope.mjs'; export * from './spectrum.mjs'; +export * from './supradough.mjs'; export * from 'superdough'; diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index 8df18bed..38b2330c 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -20,10 +20,138 @@ Pattern.prototype.supradough = function () { return this.onTrigger((_, hap, __, cps, begin) => { hap.value._begin = begin; hap.value._duration = hap.duration / cps; - - if (!doughWorklet) { - initDoughWorklet(); - } - doughWorklet.port.postMessage(hap.value); + !doughWorklet && initDoughWorklet(); + doughWorklet.port.postMessage({ spawn: hap.value }); }, 1); }; + +async function loadSampleChannels(url) { + const buffer = await fetch(url) + .then((res) => res.arrayBuffer()) + .then((buf) => getAudioContext().decodeAudioData(buf)); + // console.log('buffer', buffer, buffer.numberOfChannels); + let channels = []; + for (let i = 0; i < buffer.numberOfChannels; i++) { + channels.push(buffer.getChannelData(i)); + } + return channels; +} + +let samples = { + casio: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/high.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/low.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/noise.wav', + ], + crow: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/000_crow.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/001_crow2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/002_crow3.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/003_crow4.wav', + ], + insect: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/insect/000_everglades_conehead.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/insect/001_robust_shieldback.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/insect/002_seashore_meadow_katydid.wav', + ], + wind: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/000_wind1.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/001_wind10.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/002_wind2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/003_wind3.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/004_wind4.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/005_wind5.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/006_wind6.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/007_wind7.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/008_wind8.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/009_wind9.wav', + ], + jazz: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/000_BD.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/001_CB.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/002_FX.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/003_HH.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/004_OH.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/005_P1.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/006_P2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/007_SN.wav', + ], + metal: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/000_0.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/001_1.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/002_2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/003_3.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/004_4.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/005_5.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/006_6.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/007_7.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/008_8.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/009_9.wav', + ], + east: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/000_nipon_wood_block.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/001_ohkawa_mute.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/002_ohkawa_open.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/003_shime_hi.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/004_shime_hi_2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/005_shime_mute.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/006_taiko_1.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/007_taiko_2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/008_taiko_3.wav', + ], + space: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/000_0.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/001_1.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/002_11.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/003_12.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/004_13.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/005_14.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/006_15.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/007_16.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/008_17.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/009_18.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/010_2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/011_3.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/012_4.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/013_5.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/014_6.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/015_7.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/016_8.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/017_9.wav', + ], + numbers: [ + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/0.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/1.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/2.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/3.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/4.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/5.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/6.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/7.wav', + 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/8.wav', + ], + piano: ['https://raw.githubusercontent.com/felixroos/dough-samples/refs/heads/main/piano/A3v8.mp3'], + flute: ['https://raw.githubusercontent.com/felixroos/samples/refs/heads/main/flute/c4.mp3'], + bd: [ + 'https://raw.githubusercontent.com/geikha/tidal-drum-machines/15eac73c5e878550f91d864a4863e014799403f1/machines/RolandTR909/rolandtr909-bd/Bassdrum-01.wav', + ], +}; +// for some reason, only piano and flute work.. is it because mp3?? + +let loaded = false; +export async function doughsample() { + !doughWorklet && initDoughWorklet(); + if (loaded) { + return; + } + loaded = true; + const sampleMap = await Promise.all( + Object.entries(samples).map(async ([key, url]) => { + url = url[0]; + console.log(key, 'url', url); + return [key, await loadSampleChannels(url)]; + }), + ); + console.log('sampleMap', sampleMap); + doughWorklet.port.postMessage({ samples: sampleMap }); +} From 356d4360ecbbe09a21b697de7c563a4387b572cd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 8 Jun 2025 09:05:14 +0200 Subject: [PATCH 6/7] fix: scheduling + sample playback --- packages/supradough/dough.mjs | 14 ++++++-------- packages/webaudio/supradough.mjs | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index e42b4f8e..dc083bac 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -384,6 +384,7 @@ export class Distort { // distortion could be expressed as a function, because it's stateless export class BufferPlayer { + static samples = new Map(); channels = []; pos = 0; update(freq, channel = 0) { @@ -573,10 +574,9 @@ export class DoughVoice { if (shapes[this.s]) { const SourceClass = shapes[this.s]; this._sound = new SourceClass(); - } else if (value.samples.has(this.s)) { + } else if (BufferPlayer.samples.has(this.s)) { this._sample = new BufferPlayer(); - this._sample.channels = value.samples.get(this.s); - this._sample.pos = 0; + this._sample.channels = BufferPlayer.samples.get(this.s); } else { console.warn('sound not found', this.s); } @@ -735,7 +735,6 @@ export class DoughVoice { this._crush && (s = this._crush.update(s, this.crush)); this._distort && (s = this._distort.update(s, this.distort, this.distortvol)); - /* Math.random() > 0.99 && console.log('gate', gate); */ const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); s = s * env; @@ -762,7 +761,6 @@ export class Dough { delaysend = [0, 0]; delaytime = getDefaultValue('delaytime'); delayfeedback = getDefaultValue('delayfeedback'); - samples = new Map(); t = 0; // sampleRate: number, currentTime: number (seconds) constructor(sampleRate = 48000, currentTime = 0) { @@ -773,7 +771,7 @@ export class Dough { this._delayR = new Delay(); } loadSample(name, channels) { - this.samples.set(name, channels); + BufferPlayer.samples.set(name, channels); } scheduleSpawn(value) { if (value._begin === undefined) { @@ -783,12 +781,12 @@ export class Dough { throw new Error('[dough]: scheduleSpawn expected _duration to be set'); } value.sampleRate = this.sampleRate; - const time = value._begin; // set from supradough.mjs + // convert seconds to samples + const time = Math.floor(value._begin * this.sampleRate); // set from supradough.mjs this.schedule({ time, type: 'spawn', arg: value }); } spawn(value) { value.id = this.vid++; - value.samples = this.samples; const voice = new DoughVoice(value); this.voices.push(voice); // console.log('spawn', voice.id, 'voices:', this.voices.length); diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index 38b2330c..317f3aa3 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -25,18 +25,6 @@ Pattern.prototype.supradough = function () { }, 1); }; -async function loadSampleChannels(url) { - const buffer = await fetch(url) - .then((res) => res.arrayBuffer()) - .then((buf) => getAudioContext().decodeAudioData(buf)); - // console.log('buffer', buffer, buffer.numberOfChannels); - let channels = []; - for (let i = 0; i < buffer.numberOfChannels; i++) { - channels.push(buffer.getChannelData(i)); - } - return channels; -} - let samples = { casio: [ 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/high.wav', @@ -138,6 +126,17 @@ let samples = { }; // for some reason, only piano and flute work.. is it because mp3?? +async function loadSampleChannels(url) { + const buffer = await fetch(url) + .then((res) => res.arrayBuffer()) + .then((buf) => getAudioContext().decodeAudioData(buf)); + let channels = []; + for (let i = 0; i < buffer.numberOfChannels; i++) { + channels.push(buffer.getChannelData(i)); + } + return channels; +} + let loaded = false; export async function doughsample() { !doughWorklet && initDoughWorklet(); @@ -148,8 +147,9 @@ export async function doughsample() { const sampleMap = await Promise.all( Object.entries(samples).map(async ([key, url]) => { url = url[0]; - console.log(key, 'url', url); - return [key, await loadSampleChannels(url)]; + const channels = await loadSampleChannels(url); + // console.log(key, 'url', url, channels); + return [key, channels]; }), ); console.log('sampleMap', sampleMap); From 55aef1885117a94c4a153368f3b29f762a698db8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 8 Jun 2025 14:43:23 +0200 Subject: [PATCH 7/7] buffers can now be repitched (still with aliasing) --- packages/supradough/dough-worklet.mjs | 6 +++--- packages/supradough/dough.mjs | 22 ++++++++++++++-------- packages/webaudio/supradough.mjs | 10 ++++------ 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/supradough/dough-worklet.mjs b/packages/supradough/dough-worklet.mjs index 97a35c48..dd231562 100644 --- a/packages/supradough/dough-worklet.mjs +++ b/packages/supradough/dough-worklet.mjs @@ -10,10 +10,10 @@ class DoughProcessor extends AudioWorkletProcessor { if (event.data.spawn) { this.dough.scheduleSpawn(event.data.spawn); } else if (event.data.sample) { - this.dough.loadSample(event.data.sample, event.data.channels); + this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate); } else if (event.data.samples) { - event.data.samples.forEach(([name, channels]) => { - this.dough.loadSample(name, channels); + event.data.samples.forEach(([name, channels, sampleRate]) => { + this.dough.loadSample(name, channels, sampleRate); }); } else { console.log('unrecognized event type', event.data); diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index dc083bac..b378ee70 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -385,14 +385,16 @@ export class Distort { export class BufferPlayer { static samples = new Map(); - channels = []; + buffer; // { channels: Float32Array, sampleRate: number } pos = 0; + sampleFreq = 261.626; // middle c update(freq, channel = 0) { - if (this.pos >= this.channels[channel].length) { + if (this.pos >= this.buffer.channels[channel].length) { return 0; } - let s = this.channels[channel][this.pos]; - this.pos++; + const speed = ((freq / this.sampleFreq) * this.buffer.sampleRate) / SAMPLE_RATE; + let s = this.buffer.channels[channel][Math.floor(this.pos)]; + this.pos = this.pos + speed; return s; } } @@ -576,7 +578,8 @@ export class DoughVoice { this._sound = new SourceClass(); } else if (BufferPlayer.samples.has(this.s)) { this._sample = new BufferPlayer(); - this._sample.channels = BufferPlayer.samples.get(this.s); + const buffer = BufferPlayer.samples.get(this.s); + this._sample.buffer = buffer; } else { console.warn('sound not found', this.s); } @@ -738,7 +741,10 @@ export class DoughVoice { const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); s = s * env; - s = s * this.postgain * 0.2; + s = s * this.postgain; + if (!this._sample) { + s = s * 0.2; // turn down waveforms + } if (this.pan === 0.5) { this.l = this.r = s; // mono @@ -770,8 +776,8 @@ export class Dough { this._delayL = new Delay(); this._delayR = new Delay(); } - loadSample(name, channels) { - BufferPlayer.samples.set(name, channels); + loadSample(name, channels, sampleRate) { + BufferPlayer.samples.set(name, { channels, sampleRate }); } scheduleSpawn(value) { if (value._begin === undefined) { diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index 317f3aa3..269dcda9 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -118,7 +118,7 @@ let samples = { 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/7.wav', 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/8.wav', ], - piano: ['https://raw.githubusercontent.com/felixroos/dough-samples/refs/heads/main/piano/A3v8.mp3'], + piano: ['https://raw.githubusercontent.com/felixroos/dough-samples/refs/heads/main/piano/C3v8.mp3'], flute: ['https://raw.githubusercontent.com/felixroos/samples/refs/heads/main/flute/c4.mp3'], bd: [ 'https://raw.githubusercontent.com/geikha/tidal-drum-machines/15eac73c5e878550f91d864a4863e014799403f1/machines/RolandTR909/rolandtr909-bd/Bassdrum-01.wav', @@ -126,7 +126,7 @@ let samples = { }; // for some reason, only piano and flute work.. is it because mp3?? -async function loadSampleChannels(url) { +async function loadSampleChannels(key, url) { const buffer = await fetch(url) .then((res) => res.arrayBuffer()) .then((buf) => getAudioContext().decodeAudioData(buf)); @@ -134,7 +134,7 @@ async function loadSampleChannels(url) { for (let i = 0; i < buffer.numberOfChannels; i++) { channels.push(buffer.getChannelData(i)); } - return channels; + return [key, channels, buffer.sampleRate]; } let loaded = false; @@ -147,9 +147,7 @@ export async function doughsample() { const sampleMap = await Promise.all( Object.entries(samples).map(async ([key, url]) => { url = url[0]; - const channels = await loadSampleChannels(url); - // console.log(key, 'url', url, channels); - return [key, channels]; + return loadSampleChannels(key, url); }), ); console.log('sampleMap', sampleMap);