From 73d49f152af197bcd7e16ac1b0ac75193e3fcf5d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 16 May 2024 10:52:54 +0200 Subject: [PATCH 1/3] refactor sampler: - delegate getSampleBufferSource logic to getSampleInfo + getSampleBuffer - move buffer logic up from onTriggerSample to those functions --- packages/superdough/sampler.mjs | 115 ++++++++++++++++------------- packages/superdough/superdough.mjs | 1 + 2 files changed, 65 insertions(+), 51 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 10087fc6..4e9b8a34 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -22,47 +22,79 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resolveUrl) => { - let transpose = 0; - if (freq !== undefined && note !== undefined) { - logger('[sampler] hap has note and freq. ignoring note', 'warning'); - } - let midi = valueToMidi({ freq, note }, 36); - transpose = midi - 36; // C3 is middle C - - const ac = getAudioContext(); - +// deduces relevant info for sample loading from hap.value and sample definition +// it encapsulates the core sampler logic into a pure and synchronous function +// hapValue: Hap.value, samples: sample definition for sound "s" (values in strudel.json format) +export function getSampleInfo(hapValue, samples) { + const { s, n = 0, speed = 1.0 } = hapValue; + let midi = valueToMidi(hapValue, 36); + let transpose = midi - 36; // C3 is middle C; let sampleUrl; let index = 0; - if (Array.isArray(bank)) { - index = getSoundIndex(n, bank.length); - sampleUrl = bank[index]; + if (Array.isArray(samples)) { + index = getSoundIndex(n, samples.length); + sampleUrl = samples[index]; } else { const midiDiff = (noteA) => noteToMidi(noteA) - midi; // object format will expect keys as notes - const closest = Object.keys(bank) + const closest = Object.keys(samples) .filter((k) => !k.startsWith('_')) .reduce( (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), null, ); transpose = -midiDiff(closest); // semitones to repitch - index = getSoundIndex(n, bank[closest].length); - sampleUrl = bank[closest][index]; + index = getSoundIndex(n, samples[closest].length); + sampleUrl = samples[closest][index]; } + const label = `${s}:${index}`; + let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12); + return { transpose, sampleUrl, index, midi, label, playbackRate }; +} + +// takes hapValue and returns buffer + playbackRate. +export const getSampleBuffer = async (hapValue, samples, resolveUrl) => { + let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, samples); if (resolveUrl) { sampleUrl = await resolveUrl(sampleUrl); } - let buffer = await loadBuffer(sampleUrl, ac, s, index); + const ac = getAudioContext(); + const buffer = await loadBuffer(sampleUrl, ac, label); + + if (hapValue.unit === 'c') { + playbackRate = playbackRate * buffer.duration; + } + return { buffer, playbackRate }; +}; + +// creates playback ready AudioBufferSourceNode from hapValue +export const getSampleBufferSource = async (hapValue, samples, resolveUrl) => { + let { buffer, playbackRate } = await getSampleBuffer(hapValue, samples, resolveUrl); if (speed < 0) { // should this be cached? buffer = reverseBuffer(buffer); } + const ac = getAudioContext(); const bufferSource = ac.createBufferSource(); bufferSource.buffer = buffer; - const playbackRate = 1.0 * Math.pow(2, transpose / 12); bufferSource.playbackRate.value = playbackRate; - return bufferSource; + + const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; + + // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, + // rather than the current playback rate, so even if the sound is playing at twice its normal speed, + // the midway point through a 10-second audio buffer is still 5." + const offset = begin * bufferSource.buffer.duration; + // sound names starting with wt_ are looped automatically (wt = wavetable) + const loop = s.startsWith('wt_') ? 1 : hapValue.loop; + if (loop) { + bufferSource.loop = true; + bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset; + bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset; + } + const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; + const sliceDuration = (end - begin) * bufferDuration; + return { bufferSource, offset, bufferDuration, sliceDuration }; }; export const loadBuffer = (url, ac, s, n = 0) => { @@ -246,41 +278,29 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const cutGroups = []; -export async function onTriggerSample(t, value, onended, bank, resolveUrl) { +export async function onTriggerSample(t, value, onended, samples, resolveUrl) { let { s, - freq, - unit, nudge = 0, // TODO: is this in seconds? cut, loop, clip = undefined, // if set, samples will be cut off when the hap ends n = 0, - note, speed = 1, // sample playback speed - loopBegin = 0, - begin = 0, - loopEnd = 1, - end = 1, duration, } = value; + // load sample if (speed === 0) { // no playback return; } - loop = s.startsWith('wt_') ? 1 : value.loop; const ac = getAudioContext(); + // destructure adsr here, because the default should be different for synths and samples - let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); - //const soundfont = getSoundfontKey(s); - const time = t + nudge; - const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl); - - // vibrato - let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); + const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, samples, resolveUrl); // asny stuff above took too long? if (ac.currentTime > t) { @@ -292,26 +312,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { logger(`[sampler] could not load "${s}:${n}"`, 'error'); return; } - bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value; - if (unit === 'c') { - // are there other units? - bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * 1; //cps; - } - // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, - // rather than the current playback rate, so even if the sound is playing at twice its normal speed, - // the midway point through a 10-second audio buffer is still 5." - const offset = begin * bufferSource.buffer.duration; - if (loop) { - bufferSource.loop = true; - bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset; - bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset; - } + + // vibrato + let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); + + const time = t + nudge; bufferSource.start(time, offset); + const envGain = ac.createGain(); const node = bufferSource.connect(envGain); + + // if none of these controls is set, the duration of the sound will be set to the duration of the sample slice if (clip == null && loop == null && value.release == null) { - const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; - duration = (end - begin) * bufferDuration; + duration = sliceDuration; } let holdEnd = t + duration; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c0ba96e0..ebb54863 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -362,6 +362,7 @@ export const superdough = async (value, t, hapDuration) => { }; if (bank && s) { s = `${bank}_${s}`; + value.s = s; } // get source AudioNode From 226cf356d6e509e41edf555037767bfaf67f8e51 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 16 May 2024 11:05:38 +0200 Subject: [PATCH 2/3] fix: lint --- packages/superdough/sampler.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 4e9b8a34..552821da 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -70,7 +70,7 @@ export const getSampleBuffer = async (hapValue, samples, resolveUrl) => { // creates playback ready AudioBufferSourceNode from hapValue export const getSampleBufferSource = async (hapValue, samples, resolveUrl) => { let { buffer, playbackRate } = await getSampleBuffer(hapValue, samples, resolveUrl); - if (speed < 0) { + if (hapValue.speed < 0) { // should this be cached? buffer = reverseBuffer(buffer); } @@ -85,7 +85,7 @@ export const getSampleBufferSource = async (hapValue, samples, resolveUrl) => { // rather than the current playback rate, so even if the sound is playing at twice its normal speed, // the midway point through a 10-second audio buffer is still 5." const offset = begin * bufferSource.buffer.duration; - // sound names starting with wt_ are looped automatically (wt = wavetable) + const loop = s.startsWith('wt_') ? 1 : hapValue.loop; if (loop) { bufferSource.loop = true; From 69a5d62027ddaeeb1f61c61d7b6888ed6e465bd2 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 17 May 2024 09:42:03 +0200 Subject: [PATCH 3/3] rename "samples" to "bank" --- packages/superdough/sampler.mjs | 34 ++++++++++++++++----------------- website/src/repl/files.mjs | 6 +++--- website/src/repl/idbutils.mjs | 6 +++--- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 552821da..f5e46d6b 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -24,28 +24,28 @@ function humanFileSize(bytes, si) { // deduces relevant info for sample loading from hap.value and sample definition // it encapsulates the core sampler logic into a pure and synchronous function -// hapValue: Hap.value, samples: sample definition for sound "s" (values in strudel.json format) -export function getSampleInfo(hapValue, samples) { +// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format) +export function getSampleInfo(hapValue, bank) { const { s, n = 0, speed = 1.0 } = hapValue; let midi = valueToMidi(hapValue, 36); let transpose = midi - 36; // C3 is middle C; let sampleUrl; let index = 0; - if (Array.isArray(samples)) { - index = getSoundIndex(n, samples.length); - sampleUrl = samples[index]; + if (Array.isArray(bank)) { + index = getSoundIndex(n, bank.length); + sampleUrl = bank[index]; } else { const midiDiff = (noteA) => noteToMidi(noteA) - midi; // object format will expect keys as notes - const closest = Object.keys(samples) + const closest = Object.keys(bank) .filter((k) => !k.startsWith('_')) .reduce( (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), null, ); transpose = -midiDiff(closest); // semitones to repitch - index = getSoundIndex(n, samples[closest].length); - sampleUrl = samples[closest][index]; + index = getSoundIndex(n, bank[closest].length); + sampleUrl = bank[closest][index]; } const label = `${s}:${index}`; let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12); @@ -53,8 +53,8 @@ export function getSampleInfo(hapValue, samples) { } // takes hapValue and returns buffer + playbackRate. -export const getSampleBuffer = async (hapValue, samples, resolveUrl) => { - let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, samples); +export const getSampleBuffer = async (hapValue, bank, resolveUrl) => { + let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank); if (resolveUrl) { sampleUrl = await resolveUrl(sampleUrl); } @@ -68,8 +68,8 @@ export const getSampleBuffer = async (hapValue, samples, resolveUrl) => { }; // creates playback ready AudioBufferSourceNode from hapValue -export const getSampleBufferSource = async (hapValue, samples, resolveUrl) => { - let { buffer, playbackRate } = await getSampleBuffer(hapValue, samples, resolveUrl); +export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => { + let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl); if (hapValue.speed < 0) { // should this be cached? buffer = reverseBuffer(buffer); @@ -264,10 +264,10 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const { prebake, tag } = options; processSampleMap( sampleMap, - (key, value) => - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { + (key, bank) => + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { type: 'sample', - samples: value, + samples: bank, baseUrl, prebake, tag, @@ -278,7 +278,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const cutGroups = []; -export async function onTriggerSample(t, value, onended, samples, resolveUrl) { +export async function onTriggerSample(t, value, onended, bank, resolveUrl) { let { s, nudge = 0, // TODO: is this in seconds? @@ -300,7 +300,7 @@ export async function onTriggerSample(t, value, onended, samples, resolveUrl) { // destructure adsr here, because the default should be different for synths and samples let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); - const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, samples, resolveUrl); + const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl); // asny stuff above took too long? if (ac.currentTime > t) { diff --git a/website/src/repl/files.mjs b/website/src/repl/files.mjs index 63ac5f85..5295ba55 100644 --- a/website/src/repl/files.mjs +++ b/website/src/repl/files.mjs @@ -23,10 +23,10 @@ async function hasStrudelJson(subpath) { async function loadStrudelJson(subpath) { const contents = await readTextFile(subpath + '/strudel.json', { dir }); const sampleMap = JSON.parse(contents); - processSampleMap(sampleMap, (key, value) => { - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value, fileResolver(subpath)), { + processSampleMap(sampleMap, (key, bank) => { + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank, fileResolver(subpath)), { type: 'sample', - samples: value, + samples: bank, fileSystem: true, tag: 'local', }); diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 40219c9a..404ca647 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -49,10 +49,10 @@ export const registerSamplesFromDB = (config = userSamplesDBConfig, onComplete = }); sounds.forEach((soundPaths, key) => { - const value = Array.from(soundPaths); - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { + const bank = Array.from(soundPaths); + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { type: 'sample', - samples: value, + samples: bank, baseUrl: undefined, prebake: false, tag: undefined,