diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 9188c17c3..b195ba79c 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,5 +1,5 @@ -import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs'; -import { getAudioContext, registerSound } from './index.mjs'; +import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs'; +import { getAudioContext, registerSound, registerWaveTable } from './index.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -22,39 +22,16 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -// 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, 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(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(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, bank[closest].length); - sampleUrl = bank[closest][index]; - } - const label = `${s}:${index}`; + const { speed = 1.0 } = hapValue; + const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank); let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12); - return { transpose, sampleUrl, index, midi, label, playbackRate }; + return { transpose, url, index, midi, label, playbackRate }; } // takes hapValue and returns buffer + playbackRate. export const getSampleBuffer = async (hapValue, bank, resolveUrl) => { - let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank); + let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank); if (resolveUrl) { sampleUrl = await resolveUrl(sampleUrl); } @@ -79,14 +56,14 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => { bufferSource.buffer = buffer; bufferSource.playbackRate.value = playbackRate; - const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; + const { 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; - const loop = s.startsWith('wt_') ? 1 : hapValue.loop; + const loop = hapValue.loop; if (loop) { bufferSource.loop = true; bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset; @@ -267,16 +244,12 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option return samples(json, baseUrl || base, options); } const { prebake, tag } = options; + processSampleMap( sampleMap, - (key, bank) => - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { - type: 'sample', - samples: bank, - baseUrl, - prebake, - tag, - }), + (key, bank) => { + registerSampleSource(key, bank, { baseUrl, prebake, tag }); + }, baseUrl, ); }; @@ -366,3 +339,20 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { return handle; } + +function registerSample(key, bank, params) { + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { + type: 'sample', + samples: bank, + ...params, + }); +} + +export function registerSampleSource(key, bank, params) { + const isWavetable = key.startsWith('wt_'); + if (isWavetable) { + registerWaveTable(key, bank, params); + } else { + registerSample(key, bank, params); + } +} diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 764ebb43e..0ba095175 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -76,3 +76,31 @@ export function cycleToSeconds(cycle, cps) { export function secondsToCycle(t, cps) { return t * cps; } +// 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, bank: sample bank definition for sound "s" (values in strudel.json format) +export function getCommonSampleInfo(hapValue, bank) { + const { s, n = 0 } = hapValue; + let midi = valueToMidi(hapValue, 36); + let transpose = midi - 36; // C3 is middle C; + let url; + let index = 0; + if (Array.isArray(bank)) { + index = getSoundIndex(n, bank.length); + url = bank[index]; + } else { + const midiDiff = (noteA) => noteToMidi(noteA) - midi; + // object format will expect keys as notes + 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, bank[closest].length); + url = bank[closest][index]; + } + const label = `${s}:${index}`; + return { transpose, url, index, midi, label }; +} diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index c628549e7..c54f3a888 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -1,5 +1,5 @@ import { getAudioContext, registerSound } from './index.mjs'; -import { getSoundIndex, valueToMidi } from './util.mjs'; +import { getCommonSampleInfo, getSoundIndex, valueToMidi } from './util.mjs'; import { destroyAudioWorkletNode, getADSRValues, @@ -38,12 +38,12 @@ export const WarpMode = Object.freeze({ FLIP: 21, }); -async function loadWavetableFrames(url, label, frameLen = 256) { +async function loadWavetableFrames(url, label, frameLen = 2048) { const ac = getAudioContext(); const buf = await loadBuffer(url, ac, label); const ch0 = buf.getChannelData(0); const total = ch0.length; - const numFrames = Math.floor(total / frameLen); + const numFrames = Math.max(1, Math.floor(total / frameLen)); const frames = new Array(numFrames); for (let i = 0; i < numFrames; i++) { const start = i * frameLen; @@ -86,14 +86,8 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -export function getTableInfo(hapValue, tableUrls) { - const { s, n = 0 } = hapValue; - let midi = valueToMidi(hapValue, 36); - let transpose = midi - 36; // C3 is middle C; - const index = getSoundIndex(n, tableUrls.length); - const tableUrl = tableUrls[index]; - const label = `${s}:${index}`; - return { transpose, tableUrl, index, midi, label }; +export function getTableInfo(hapValue, urls) { + return getCommonSampleInfo(hapValue, urls); } const loadBuffer = (url, ac, label) => { @@ -140,15 +134,18 @@ const _processTables = (json, baseUrl, frameLen) => { baseUrl = githubPath(baseUrl, ''); } value = value.map((v) => baseUrl + v); - registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, value, frameLen), { - type: 'wavetable', - tables: value, - baseUrl, - frameLen, - }); + registerWaveTable(key, value, { baseUrl, frameLen }); }); }; +export function registerWaveTable(key, bank, params) { + registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, bank, params?.frameLen ?? 2048), { + type: 'wavetable', + tables: bank, + ...params, + }); +} + /** * Loads a collection of wavetables to use with `s` * @@ -179,7 +176,7 @@ export const tables = async (url, frameLen, json) => { }); }; -async function onTriggerSynth(t, value, onended, bank, frameLen) { +export async function onTriggerSynth(t, value, onended, bank, frameLen) { const { s, n = 0, duration } = value; const ac = getAudioContext(); const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); @@ -188,8 +185,8 @@ async function onTriggerSynth(t, value, onended, bank, frameLen) { wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE; } const frequency = getFrequencyFromValue(value); - const { tableUrl, label } = getTableInfo(value, bank); - const payload = await loadWavetableFrames(tableUrl, label, frameLen); + const { url, label } = getTableInfo(value, bank); + const payload = await loadWavetableFrames(url, label, frameLen); const holdEnd = t + duration; const envEnd = holdEnd + release + 0.01; const source = getWorklet( diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index af8a4a633..91eb68634 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1049,7 +1049,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { { name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, { name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, { name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 }, - { name: 'detune', defaultValue: 0 }, + { name: 'detune', defaultValue: 0.18 }, { name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warpMode', defaultValue: 0 }, @@ -1239,6 +1239,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const outL = outputs[0][0]; const outR = outputs[0][1] || outputs[0][0]; + const gainAdjustment = 0.3; if (!this.tables) { outL.fill(0); @@ -1284,8 +1285,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { s = -s; } - outL[i] += (s * gainL) / Math.sqrt(voices); - outR[i] += (s * gainR) / Math.sqrt(voices); + outL[i] += ((s * gainL) / Math.sqrt(voices)) * gainAdjustment; + outR[i] += ((s * gainR) / Math.sqrt(voices)) * gainAdjustment; this.phase[n] = wrapPhase(this.phase[n] + dPhase); } } diff --git a/website/public/uzu-wavetables.json b/website/public/uzu-wavetables.json new file mode 100644 index 000000000..be8a4e16e --- /dev/null +++ b/website/public/uzu-wavetables.json @@ -0,0 +1,22 @@ +{ + "_base": "https://raw.githubusercontent.com/tidalcycles/uzu-wavetables/main/", + "wt_digital": [ + "wt_digital/wt_bad_day.wav", + "wt_digital/wt_basique.wav", + "wt_digital/wt_crickets.wav", + "wt_digital/wt_curses.wav", + "wt_digital/wt_earl_grey.wav", + "wt_digital/wt_echoes.wav", + "wt_digital/wt_glimmer.wav", + "wt_digital/wt_majick.wav", + "wt_digital/wt_meditation.wav", + "wt_digital/wt_morgana.wav", + "wt_digital/wt_red_alert.wav", + "wt_digital/wt_sad_piano.wav", + "wt_digital/wt_shook.wav", + "wt_digital/wt_sludge.wav", + "wt_digital/wt_squelch.wav", + "wt_digital/wt_summer.wav", + "wt_digital/wt_wasp.wav" + ] +} \ No newline at end of file diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index f26ee0479..d87d649c2 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -1,4 +1,4 @@ -import { registerSound, onTriggerSample } from '@strudel/webaudio'; +import { registerSampleSource } from '@strudel/webaudio'; import { isAudioFile } from './files.mjs'; import { logger } from '@strudel/core'; @@ -76,13 +76,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = }) .map((title) => titlePathMap.get(title)); - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { - type: 'sample', - samples: value, - baseUrl: undefined, - prebake: false, - tag: undefined, - }); + registerSampleSource(key, value, { prebake: false }); }); logger('imported sounds registered!', 'success'); diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index fac6f5bb6..1fbc84021 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -32,6 +32,9 @@ export async function prebake() { prebake: true, tag: 'drum-machines', }), + samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, { + prebake: true, + }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples( {