From feab3f5c867a198a5acf87a2ea70a25671913ce8 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 8 Feb 2026 19:25:10 -0500 Subject: [PATCH 1/3] freq --- packages/superdough/sampler.mjs | 33 +++++++++++++++++--- packages/superdough/util.mjs | 55 +++++++++++++++++++++++++++------ website/src/repl/idbutils.mjs | 25 +++++++-------- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index e7aee294f..c376f6280 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,4 +1,4 @@ -import { getBaseURL, getCommonSampleInfo } from './util.mjs'; +import { getBaseURL, getCommonSampleInfo, noteToFreq } from './util.mjs'; import { registerSound, registerWaveTable } from './index.mjs'; import { getAudioContext } from './audioContext.mjs'; import { @@ -10,6 +10,7 @@ import { releaseAudioNode, } from './helpers.mjs'; import { logger } from './logger.mjs'; +import { note2midi } from 'node_modules/@strudel/tonal/tonleiter.mjs'; const bufferCache = {}; // string: Promise const loadCache = {}; // string: Promise @@ -30,10 +31,13 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -export function getSampleInfo(hapValue, bank) { +function getSampleInfo(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); + const { transpose, url, index, midi, label, baseFrequency } = getCommonSampleInfo(hapValue, bank); + const relativeBaseFreq = noteToFreq('C'); + + const frequencyAdjustment = relativeBaseFreq / baseFrequency; + const playbackRate = (Math.abs(speed) * Math.pow(2, transpose / 12)) * frequencyAdjustment; return { transpose, url, index, midi, label, playbackRate }; } @@ -360,6 +364,7 @@ function registerSample(key, bank, params) { } export function registerSampleSource(key, bank, params) { + const isWavetable = key.startsWith('wt_'); if (isWavetable) { registerWaveTable(key, bank, params); @@ -367,3 +372,23 @@ export function registerSampleSource(key, bank, params) { registerSample(key, bank, params); } } + +function extractNoteFromString(str) { + const regex = /_([a-gA-G])([#b])?([1-9])?\b/; + const match = str.match(regex); + if (match == null) { + return { note: "C", base: "C", accidental: undefined, octave: 3 } + } + const base = match[1].toUpperCase(); + const accidental = match[2] ?? "" + const octave_str = match[3] ?? ""; + const octave = Number(octave_str) + return { note: base + accidental + octave_str, base, accidental, octave } + +} + + +export function extractBaseFrequencyFromString(str) { + const { note } = extractNoteFromString(str); + return noteToFreq(note) +} \ No newline at end of file diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index caa58fa80..841cb8655 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -2,7 +2,12 @@ import { logger } from './logger.mjs'; // currently duplicate with core util.mjs to skip dependency // TODO: add separate util module? - +/** + * + * @typedef {Object} SampleMetaData + * @property {string} url + * @property {baseFrequency} number + */ export const tokenizeNote = (note) => { if (typeof note !== 'string') { return []; @@ -19,7 +24,12 @@ const accs = { '#': 1, b: -1, s: 1, f: -1 }; export const getAccidentalsOffset = (accidentals) => { return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0; }; - +/** + * + * @param {string} note + * @param {number} defaultOctave + * @returns {number} + */ export const noteToMidi = (note, defaultOctave = 3) => { const [pc, acc, oct = defaultOctave] = tokenizeNote(note); if (!pc) { @@ -29,17 +39,34 @@ export const noteToMidi = (note, defaultOctave = 3) => { const offset = getAccidentalsOffset(acc); return (Number(oct) + 1) * 12 + chroma + offset; }; +/** + * + * @param {number} n + * @returns {number} + */ export const midiToFreq = (n) => { return Math.pow(2, (n - 69) / 12) * 440; }; export const clamp = (num, min, max) => Math.min(Math.max(num, min), max); + export const freqToMidi = (freq) => { return (12 * Math.log(freq / 440)) / Math.LN2 + 69; }; -export const valueToMidi = (value, fallbackValue) => { - if (typeof value !== 'object') { +/** + * + * @param {string} note + * @param {number} defaultOctave + * @returns {number} + */ + +export const noteToFreq = (note, defaultOctave = 3) => { + return midiToFreq(noteToMidi(note, defaultOctave)) + +} +function __valueToMidi(value) { + if (typeof value !== 'object') { throw new Error('valueToMidi: expected object value'); } let { freq, note } = value; @@ -52,10 +79,15 @@ export const valueToMidi = (value, fallbackValue) => { if (typeof note === 'number') { return note; } - if (!fallbackValue) { + return +} + +export const valueToMidi = (value, fallbackValue) => { + const parsedValue = __valueToMidi(value) ?? fallbackValue; + if (parsedValue == null) { throw new Error('valueToMidi: expected freq or note to be set'); } - return fallbackValue; + return parsedValue }; export function nanFallback(value, fallback = 0, silent) { @@ -88,11 +120,11 @@ 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; + let samplemeta; if (Array.isArray(bank)) { index = getSoundIndex(n, bank.length); - url = bank[index]; + samplemeta = bank[index] } else { const midiDiff = (noteA) => noteToMidi(noteA) - midi; // object format will expect keys as notes @@ -104,10 +136,13 @@ export function getCommonSampleInfo(hapValue, bank) { ); transpose = -midiDiff(closest); // semitones to repitch index = getSoundIndex(n, bank[closest].length); - url = bank[closest][index]; + samplemeta = bank[closest][index]; } const label = `${s}:${index}`; - return { transpose, url, index, midi, label }; + + + + return { transpose, index, midi, label, ...samplemeta }; } /** Selects entries from `source` and renames them via `map` diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index e1ca3a604..aad59c953 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -1,4 +1,4 @@ -import { registerSampleSource } from '@strudel/webaudio'; +import { extractBaseFrequencyFromString, registerSampleSource } from '@strudel/webaudio'; import { isAudioFile } from './files.mjs'; import { logger } from '@strudel/core'; @@ -28,7 +28,7 @@ export function clearIDB(dbName) { } // queries the DB, and registers the sounds so they can be played -export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) { +export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => { }) { openDB(config, (objectStore) => { const query = objectStore.getAll(); query.onerror = (e) => { @@ -47,7 +47,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = Promise.all( [...soundFiles] .sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' })) - .map((soundFile, i) => { + .map((soundFile) => { const title = soundFile.title; if (!isAudioFile(title)) { return; @@ -58,25 +58,24 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; const blob = soundFile.blob; - return blobToDataUrl(blob).then((soundPath) => { - const titlePathMap = sounds.get(parentDirectory) ?? new Map(); + return blobToDataUrl(blob).then((path) => { + const sampleInfoMap = sounds.get(parentDirectory) ?? new Map(); + const baseFrequency = extractBaseFrequencyFromString(title) + sampleInfoMap.set(title, { url: path, baseFrequency }); - titlePathMap.set(title, soundPath); - - sounds.set(parentDirectory, titlePathMap); + sounds.set(parentDirectory, sampleInfoMap); return; }); }), ) .then(() => { - sounds.forEach((titlePathMap, key) => { - const value = Array.from(titlePathMap.keys()) + sounds.forEach((sampleInfoMap, key) => { + const bank = Array.from(sampleInfoMap.keys()) .sort((a, b) => { return a.localeCompare(b); }) - .map((title) => titlePathMap.get(title)); - - registerSampleSource(key, value, { prebake: false }); + .map((title) => sampleInfoMap.get(title)); + registerSampleSource(key, bank, { prebake: false }); }); logger('imported sounds registered!', 'success'); From 4f05674cb3a471c7d2a9c6532fb278eda58f3737 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 8 Feb 2026 20:32:59 -0500 Subject: [PATCH 2/3] working --- packages/superdough/sampler.mjs | 28 ++++++----------- packages/superdough/util.mjs | 54 +++++++++++++++++---------------- website/src/repl/idbutils.mjs | 8 ++--- 3 files changed, 41 insertions(+), 49 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index c376f6280..188732656 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,4 +1,4 @@ -import { getBaseURL, getCommonSampleInfo, noteToFreq } from './util.mjs'; +import { BASE_MIDI_NOTE, getBaseURL, getCommonSampleInfo, noteToFreq, noteToMidi } from './util.mjs'; import { registerSound, registerWaveTable } from './index.mjs'; import { getAudioContext } from './audioContext.mjs'; import { @@ -34,10 +34,8 @@ function humanFileSize(bytes, si) { function getSampleInfo(hapValue, bank) { const { speed = 1.0 } = hapValue; const { transpose, url, index, midi, label, baseFrequency } = getCommonSampleInfo(hapValue, bank); - const relativeBaseFreq = noteToFreq('C'); - const frequencyAdjustment = relativeBaseFreq / baseFrequency; - const playbackRate = (Math.abs(speed) * Math.pow(2, transpose / 12)) * frequencyAdjustment; + const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12); return { transpose, url, index, midi, label, playbackRate }; } @@ -160,7 +158,7 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') if (baseUrl.startsWith('github:')) { baseUrl = githubPath(baseUrl, ''); } - const fullUrl = (v) => baseUrl + v; + const fullUrl = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) }); if (Array.isArray(value)) { //return [key, value.map(replaceUrl)]; value = value.map(fullUrl); @@ -364,7 +362,6 @@ function registerSample(key, bank, params) { } export function registerSampleSource(key, bank, params) { - const isWavetable = key.startsWith('wt_'); if (isWavetable) { registerWaveTable(key, bank, params); @@ -373,22 +370,15 @@ export function registerSampleSource(key, bank, params) { } } -function extractNoteFromString(str) { +export function extractMidiNoteFromString(str) { const regex = /_([a-gA-G])([#b])?([1-9])?\b/; const match = str.match(regex); if (match == null) { - return { note: "C", base: "C", accidental: undefined, octave: 3 } + return BASE_MIDI_NOTE; } const base = match[1].toUpperCase(); - const accidental = match[2] ?? "" - const octave_str = match[3] ?? ""; - const octave = Number(octave_str) - return { note: base + accidental + octave_str, base, accidental, octave } - + const accidental = match[2] ?? ''; + const octave_str = match[3] ?? ''; + const parsedVal = base + accidental + octave_str; + return noteToMidi(parsedVal); } - - -export function extractBaseFrequencyFromString(str) { - const { note } = extractNoteFromString(str); - return noteToFreq(note) -} \ No newline at end of file diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 841cb8655..8fc6752ce 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -3,10 +3,10 @@ import { logger } from './logger.mjs'; // currently duplicate with core util.mjs to skip dependency // TODO: add separate util module? /** - * - * @typedef {Object} SampleMetaData - * @property {string} url - * @property {baseFrequency} number + * + * @typedef {Object} SampleMetaData + * @property {string} url + * @property {baseFrequency} number */ export const tokenizeNote = (note) => { if (typeof note !== 'string') { @@ -25,9 +25,9 @@ export const getAccidentalsOffset = (accidentals) => { return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0; }; /** - * - * @param {string} note - * @param {number} defaultOctave + * + * @param {string} note + * @param {number} defaultOctave * @returns {number} */ export const noteToMidi = (note, defaultOctave = 3) => { @@ -40,8 +40,8 @@ export const noteToMidi = (note, defaultOctave = 3) => { return (Number(oct) + 1) * 12 + chroma + offset; }; /** - * - * @param {number} n + * + * @param {number} n * @returns {number} */ export const midiToFreq = (n) => { @@ -49,24 +49,22 @@ export const midiToFreq = (n) => { }; export const clamp = (num, min, max) => Math.min(Math.max(num, min), max); - export const freqToMidi = (freq) => { return (12 * Math.log(freq / 440)) / Math.LN2 + 69; }; /** - * - * @param {string} note - * @param {number} defaultOctave + * + * @param {string} note + * @param {number} defaultOctave * @returns {number} */ export const noteToFreq = (note, defaultOctave = 3) => { - return midiToFreq(noteToMidi(note, defaultOctave)) - -} + return midiToFreq(noteToMidi(note, defaultOctave)); +}; function __valueToMidi(value) { - if (typeof value !== 'object') { + if (typeof value !== 'object') { throw new Error('valueToMidi: expected object value'); } let { freq, note } = value; @@ -79,7 +77,7 @@ function __valueToMidi(value) { if (typeof note === 'number') { return note; } - return + return; } export const valueToMidi = (value, fallbackValue) => { @@ -87,7 +85,7 @@ export const valueToMidi = (value, fallbackValue) => { if (parsedValue == null) { throw new Error('valueToMidi: expected freq or note to be set'); } - return parsedValue + return parsedValue; }; export function nanFallback(value, fallback = 0, silent) { @@ -116,15 +114,18 @@ export function secondsToCycle(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 const BASE_MIDI_NOTE = 36; + export function getCommonSampleInfo(hapValue, bank) { const { s, n = 0 } = hapValue; - let midi = valueToMidi(hapValue, 36); - let transpose = midi - 36; // C3 is middle C; + const maybeMidiNote = __valueToMidi(hapValue); + const midi = maybeMidiNote ?? BASE_MIDI_NOTE; + let transpose = midi - BASE_MIDI_NOTE; // C3 is middle C; let index = 0; let samplemeta; if (Array.isArray(bank)) { index = getSoundIndex(n, bank.length); - samplemeta = bank[index] + samplemeta = bank[index]; } else { const midiDiff = (noteA) => noteToMidi(noteA) - midi; // object format will expect keys as notes @@ -136,13 +137,14 @@ export function getCommonSampleInfo(hapValue, bank) { ); transpose = -midiDiff(closest); // semitones to repitch index = getSoundIndex(n, bank[closest].length); - samplemeta = bank[closest][index]; + samplemeta = bank[closest][index]; } const label = `${s}:${index}`; + if (maybeMidiNote != null) { + transpose = transpose + (BASE_MIDI_NOTE - samplemeta.midi); + } - - - return { transpose, index, midi, label, ...samplemeta }; + return { transpose, index, midi, label, url: samplemeta.url }; } /** Selects entries from `source` and renames them via `map` diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index aad59c953..4022da883 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -1,4 +1,4 @@ -import { extractBaseFrequencyFromString, registerSampleSource } from '@strudel/webaudio'; +import { extractMidiNoteFromString, registerSampleSource } from '@strudel/webaudio'; import { isAudioFile } from './files.mjs'; import { logger } from '@strudel/core'; @@ -28,7 +28,7 @@ export function clearIDB(dbName) { } // queries the DB, and registers the sounds so they can be played -export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => { }) { +export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) { openDB(config, (objectStore) => { const query = objectStore.getAll(); query.onerror = (e) => { @@ -60,8 +60,8 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = return blobToDataUrl(blob).then((path) => { const sampleInfoMap = sounds.get(parentDirectory) ?? new Map(); - const baseFrequency = extractBaseFrequencyFromString(title) - sampleInfoMap.set(title, { url: path, baseFrequency }); + const midi = extractMidiNoteFromString(title); + sampleInfoMap.set(title, { url: path, midi }); sounds.set(parentDirectory, sampleInfoMap); return; From 0b69ce21c70912ca0920cdba9624ef4e1b80bd20 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 8 Feb 2026 21:11:10 -0500 Subject: [PATCH 3/3] jsdoc --- packages/superdough/sampler.mjs | 13 ++++++++++++- packages/superdough/util.mjs | 7 +------ website/src/repl/idbutils.mjs | 4 +++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 188732656..e0c6fef44 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -10,11 +10,17 @@ import { releaseAudioNode, } from './helpers.mjs'; import { logger } from './logger.mjs'; -import { note2midi } from 'node_modules/@strudel/tonal/tonleiter.mjs'; const bufferCache = {}; // string: Promise const loadCache = {}; // string: Promise +/** + * + * @typedef {Object} SampleMetaData + * @property {string} url + * @property {midi} number + */ + export const getCachedBuffer = (url) => bufferCache[url]; function humanFileSize(bytes, si) { @@ -158,6 +164,11 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') if (baseUrl.startsWith('github:')) { baseUrl = githubPath(baseUrl, ''); } + /** + * + * @param {string} v + * @returns {SampleMetaData} + */ const fullUrl = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) }); if (Array.isArray(value)) { //return [key, value.map(replaceUrl)]; diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 8fc6752ce..022c328c0 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -2,12 +2,7 @@ import { logger } from './logger.mjs'; // currently duplicate with core util.mjs to skip dependency // TODO: add separate util module? -/** - * - * @typedef {Object} SampleMetaData - * @property {string} url - * @property {baseFrequency} number - */ + export const tokenizeNote = (note) => { if (typeof note !== 'string') { return []; diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 4022da883..bcd721832 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -61,7 +61,9 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = return blobToDataUrl(blob).then((path) => { const sampleInfoMap = sounds.get(parentDirectory) ?? new Map(); const midi = extractMidiNoteFromString(title); - sampleInfoMap.set(title, { url: path, midi }); + /** @type {import('@strudel/webaudio').SampleMetaData} */ + const samplemetadata = { url: path, midi }; + sampleInfoMap.set(title, samplemetadata); sounds.set(parentDirectory, sampleInfoMap); return;