From 2b3b6389431f9b38f0d26da37f0eac202c38f39f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 26 Sep 2025 21:47:45 -0500 Subject: [PATCH 1/5] sample source for WT --- packages/superdough/sampler.mjs | 38 ++++++++++++++++++++++--------- packages/superdough/util.mjs | 1 + packages/superdough/wavetable.mjs | 17 ++++++++------ packages/superdough/worklets.mjs | 5 ++-- website/src/repl/idbutils.mjs | 11 +++------ 5 files changed, 44 insertions(+), 28 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 9188c17c3..0e9633d4d 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 { getAudioContext, registerSound, registerWaveTable } from './index.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -79,14 +79,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 +267,13 @@ 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) => { + registerSample(key, bank, { baseUrl, prebake, tag }) + }, baseUrl, ); }; @@ -366,3 +363,22 @@ 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) + } + +} \ No newline at end of file diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 764ebb43e..80dd31a9d 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -76,3 +76,4 @@ export function cycleToSeconds(cycle, cps) { export function secondsToCycle(t, cps) { return t * cps; } + diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index c628549e7..067656045 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -140,15 +140,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 +182,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]); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index af8a4a633..14093d098 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1239,6 +1239,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const outL = outputs[0][0]; const outR = outputs[0][1] || outputs[0][0]; + const gainAdjustment = .15; 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/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index f26ee0479..5ac604559 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,8 @@ 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'); From 4bd103e61200fe888196807b7c43a9ff206cb38c Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 27 Sep 2025 11:31:43 -0400 Subject: [PATCH 2/5] gain adj --- packages/superdough/worklets.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 14093d098..826e96104 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: .18 }, { name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warpMode', defaultValue: 0 }, From a9957b45e5ce101fe3142d9dbeb6112cacc188a8 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 27 Sep 2025 13:22:21 -0400 Subject: [PATCH 3/5] suppoert existing wt_ api --- packages/superdough/sampler.mjs | 2 +- website/public/uzu-wavetables.json | 22 ++++++++++++++++++++++ website/src/repl/prebake.mjs | 3 +++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 website/public/uzu-wavetables.json diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 0e9633d4d..ef4bdc542 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -272,7 +272,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option processSampleMap( sampleMap, (key, bank) => { - registerSample(key, bank, { baseUrl, prebake, tag }) + registerSampleSource(key, bank, { baseUrl, prebake, tag }) }, baseUrl, ); 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/prebake.mjs b/website/src/repl/prebake.mjs index fac6f5bb6..0b552b871 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( { From 626a99ba5bbaa7032bf5ef62fe2c2d9849ed911a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 27 Sep 2025 15:22:54 -0400 Subject: [PATCH 4/5] supports old documented way of loading wavetables: --- packages/superdough/sampler.mjs | 33 +++++-------------------------- packages/superdough/util.mjs | 29 ++++++++++++++++++++++++++- packages/superdough/wavetable.mjs | 20 +++++++------------ 3 files changed, 40 insertions(+), 42 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index ef4bdc542..02e1fb334 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,4 +1,4 @@ -import { noteToMidi, valueToMidi, getSoundIndex } from './util.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); } diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 80dd31a9d..1886c55e5 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -76,4 +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 }; +} \ No newline at end of file diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 067656045..a7e708043 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) => { @@ -191,8 +185,8 @@ export 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( From 774372a339d929d8826187efc90afe5ae7e5e337 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 27 Sep 2025 15:29:55 -0400 Subject: [PATCH 5/5] format --- packages/superdough/sampler.mjs | 19 ++++++++----------- packages/superdough/util.mjs | 4 ++-- packages/superdough/wavetable.mjs | 10 +++++----- packages/superdough/worklets.mjs | 4 ++-- website/src/repl/idbutils.mjs | 3 +-- website/src/repl/prebake.mjs | 2 +- 6 files changed, 19 insertions(+), 23 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 02e1fb334..b195ba79c 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -23,8 +23,8 @@ function humanFileSize(bytes, si) { } export function getSampleInfo(hapValue, bank) { - const { speed = 1.0 } = hapValue; - const {transpose, url, index, midi, label} = getCommonSampleInfo(hapValue, bank) + 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, url, index, midi, label, playbackRate }; } @@ -245,11 +245,10 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option } const { prebake, tag } = options; - processSampleMap( sampleMap, (key, bank) => { - registerSampleSource(key, bank, { baseUrl, prebake, tag }) + registerSampleSource(key, bank, { baseUrl, prebake, tag }); }, baseUrl, ); @@ -341,21 +340,19 @@ 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 - }) + ...params, + }); } export function registerSampleSource(key, bank, params) { const isWavetable = key.startsWith('wt_'); if (isWavetable) { - registerWaveTable(key,bank, params) + registerWaveTable(key, bank, params); } else { - registerSample(key, bank, params) + registerSample(key, bank, params); } - -} \ No newline at end of file +} diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 1886c55e5..0ba095175 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -100,7 +100,7 @@ export function getCommonSampleInfo(hapValue, bank) { 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 }; -} \ No newline at end of file +} diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index a7e708043..c54f3a888 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -43,7 +43,7 @@ async function loadWavetableFrames(url, label, frameLen = 2048) { const buf = await loadBuffer(url, ac, label); const ch0 = buf.getChannelData(0); const total = ch0.length; - const numFrames = Math.max(1,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; @@ -87,7 +87,7 @@ function humanFileSize(bytes, si) { } export function getTableInfo(hapValue, urls) { - return getCommonSampleInfo(hapValue,urls) + return getCommonSampleInfo(hapValue, urls); } const loadBuffer = (url, ac, label) => { @@ -134,15 +134,15 @@ const _processTables = (json, baseUrl, frameLen) => { baseUrl = githubPath(baseUrl, ''); } value = value.map((v) => baseUrl + v); - registerWaveTable(key,value, {baseUrl, frameLen}) + registerWaveTable(key, value, { baseUrl, frameLen }); }); }; -export function registerWaveTable(key, bank, params) { +export function registerWaveTable(key, bank, params) { registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, bank, params?.frameLen ?? 2048), { type: 'wavetable', tables: bank, - ...params + ...params, }); } diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 826e96104..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: .18 }, + { 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,7 +1239,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const outL = outputs[0][0]; const outR = outputs[0][1] || outputs[0][0]; - const gainAdjustment = .15; + const gainAdjustment = 0.3; if (!this.tables) { outL.fill(0); diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 5ac604559..d87d649c2 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -76,8 +76,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = }) .map((title) => titlePathMap.get(title)); - registerSampleSource(key,value, {prebake: false}) - + 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 0b552b871..1fbc84021 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -33,7 +33,7 @@ export async function prebake() { tag: 'drum-machines', }), samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, { - prebake: true, + prebake: true, }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples(