Compare commits

...

5 Commits

Author SHA1 Message Date
Felix Roos 12fac86012 rename 2026-02-15 00:42:21 +01:00
Felix Roos cea6301ffd restore export 2026-02-15 00:40:13 +01:00
Jade (Rose) Rowland 0b69ce21c7 jsdoc 2026-02-08 21:11:10 -05:00
Jade (Rose) Rowland 4f05674cb3 working 2026-02-08 20:32:59 -05:00
Jade (Rose) Rowland feab3f5c86 freq 2026-02-08 19:25:10 -05:00
3 changed files with 85 additions and 27 deletions
+30 -5
View File
@@ -1,4 +1,4 @@
import { getBaseURL, getCommonSampleInfo } from './util.mjs'; import { BASE_MIDI_NOTE, getBaseURL, getCommonSampleInfo, noteToFreq, noteToMidi } from './util.mjs';
import { registerSound, registerWaveTable } from './index.mjs'; import { registerSound, registerWaveTable } from './index.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { import {
@@ -14,6 +14,13 @@ import { logger } from './logger.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer> const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer> const loadCache = {}; // string: Promise<ArrayBuffer>
/**
*
* @typedef {Object} SampleMetaData
* @property {string} url
* @property {midi} number
*/
export const getCachedBuffer = (url) => bufferCache[url]; export const getCachedBuffer = (url) => bufferCache[url];
function humanFileSize(bytes, si) { function humanFileSize(bytes, si) {
@@ -33,7 +40,7 @@ function humanFileSize(bytes, si) {
export function getSampleInfo(hapValue, bank) { export function getSampleInfo(hapValue, bank) {
const { speed = 1.0 } = hapValue; const { speed = 1.0 } = hapValue;
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank); const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12); const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, url, index, midi, label, playbackRate }; return { transpose, url, index, midi, label, playbackRate };
} }
@@ -156,15 +163,20 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '')
if (baseUrl.startsWith('github:')) { if (baseUrl.startsWith('github:')) {
baseUrl = githubPath(baseUrl, ''); baseUrl = githubPath(baseUrl, '');
} }
const fullUrl = (v) => baseUrl + v; /**
*
* @param {string} v
* @returns {SampleMetaData}
*/
const getMetaData = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) });
if (Array.isArray(value)) { if (Array.isArray(value)) {
//return [key, value.map(replaceUrl)]; //return [key, value.map(replaceUrl)];
value = value.map(fullUrl); value = value.map(getMetaData);
} else { } else {
// must be object // must be object
value = Object.fromEntries( value = Object.fromEntries(
Object.entries(value).map(([note, samples]) => { Object.entries(value).map(([note, samples]) => {
return [note, (typeof samples === 'string' ? [samples] : samples).map(fullUrl)]; return [note, (typeof samples === 'string' ? [samples] : samples).map(getMetaData)];
}), }),
); );
} }
@@ -367,3 +379,16 @@ export function registerSampleSource(key, bank, params) {
registerSample(key, bank, params); registerSample(key, bank, params);
} }
} }
export function extractMidiNoteFromString(str) {
const regex = /_([a-gA-G])([#b])?([1-9])?\b/;
const match = str.match(regex);
if (match == null) {
return BASE_MIDI_NOTE;
}
const base = match[1].toUpperCase();
const accidental = match[2] ?? '';
const octave_str = match[3] ?? '';
const parsedVal = base + accidental + octave_str;
return noteToMidi(parsedVal);
}
+42 -10
View File
@@ -19,7 +19,12 @@ const accs = { '#': 1, b: -1, s: 1, f: -1 };
export const getAccidentalsOffset = (accidentals) => { export const getAccidentalsOffset = (accidentals) => {
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0; 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) => { export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note); const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
if (!pc) { if (!pc) {
@@ -29,6 +34,11 @@ export const noteToMidi = (note, defaultOctave = 3) => {
const offset = getAccidentalsOffset(acc); const offset = getAccidentalsOffset(acc);
return (Number(oct) + 1) * 12 + chroma + offset; return (Number(oct) + 1) * 12 + chroma + offset;
}; };
/**
*
* @param {number} n
* @returns {number}
*/
export const midiToFreq = (n) => { export const midiToFreq = (n) => {
return Math.pow(2, (n - 69) / 12) * 440; return Math.pow(2, (n - 69) / 12) * 440;
}; };
@@ -38,7 +48,17 @@ export const freqToMidi = (freq) => {
return (12 * Math.log(freq / 440)) / Math.LN2 + 69; return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
}; };
export const valueToMidi = (value, fallbackValue) => { /**
*
* @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') { if (typeof value !== 'object') {
throw new Error('valueToMidi: expected object value'); throw new Error('valueToMidi: expected object value');
} }
@@ -52,10 +72,15 @@ export const valueToMidi = (value, fallbackValue) => {
if (typeof note === 'number') { if (typeof note === 'number') {
return note; 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'); throw new Error('valueToMidi: expected freq or note to be set');
} }
return fallbackValue; return parsedValue;
}; };
export function nanFallback(value, fallback = 0, silent) { export function nanFallback(value, fallback = 0, silent) {
@@ -84,15 +109,18 @@ export function secondsToCycle(t, cps) {
// deduces relevant info for sample loading from hap.value and sample definition // deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function // 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) // 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) { export function getCommonSampleInfo(hapValue, bank) {
const { s, n = 0 } = hapValue; const { s, n = 0 } = hapValue;
let midi = valueToMidi(hapValue, 36); const maybeMidiNote = __valueToMidi(hapValue);
let transpose = midi - 36; // C3 is middle C; const midi = maybeMidiNote ?? BASE_MIDI_NOTE;
let url; let transpose = midi - BASE_MIDI_NOTE; // C3 is middle C;
let index = 0; let index = 0;
let samplemeta;
if (Array.isArray(bank)) { if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length); index = getSoundIndex(n, bank.length);
url = bank[index]; samplemeta = bank[index];
} else { } else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi; const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes // object format will expect keys as notes
@@ -104,10 +132,14 @@ export function getCommonSampleInfo(hapValue, bank) {
); );
transpose = -midiDiff(closest); // semitones to repitch transpose = -midiDiff(closest); // semitones to repitch
index = getSoundIndex(n, bank[closest].length); index = getSoundIndex(n, bank[closest].length);
url = bank[closest][index]; samplemeta = bank[closest][index];
} }
const label = `${s}:${index}`; const label = `${s}:${index}`;
return { transpose, url, index, midi, label }; if (maybeMidiNote != null) {
transpose = transpose + (BASE_MIDI_NOTE - samplemeta.midi);
}
return { transpose, index, midi, label, url: samplemeta.url };
} }
/** Selects entries from `source` and renames them via `map` /** Selects entries from `source` and renames them via `map`
+13 -12
View File
@@ -1,4 +1,4 @@
import { registerSampleSource } from '@strudel/webaudio'; import { extractMidiNoteFromString, registerSampleSource } from '@strudel/webaudio';
import { isAudioFile } from './files.mjs'; import { isAudioFile } from './files.mjs';
import { logger } from '@strudel/core'; import { logger } from '@strudel/core';
@@ -47,7 +47,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
Promise.all( Promise.all(
[...soundFiles] [...soundFiles]
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' })) .sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
.map((soundFile, i) => { .map((soundFile) => {
const title = soundFile.title; const title = soundFile.title;
if (!isAudioFile(title)) { if (!isAudioFile(title)) {
return; return;
@@ -58,25 +58,26 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
const blob = soundFile.blob; const blob = soundFile.blob;
return blobToDataUrl(blob).then((soundPath) => { return blobToDataUrl(blob).then((path) => {
const titlePathMap = sounds.get(parentDirectory) ?? new Map(); const sampleInfoMap = sounds.get(parentDirectory) ?? new Map();
const midi = extractMidiNoteFromString(title);
/** @type {import('@strudel/webaudio').SampleMetaData} */
const samplemetadata = { url: path, midi };
sampleInfoMap.set(title, samplemetadata);
titlePathMap.set(title, soundPath); sounds.set(parentDirectory, sampleInfoMap);
sounds.set(parentDirectory, titlePathMap);
return; return;
}); });
}), }),
) )
.then(() => { .then(() => {
sounds.forEach((titlePathMap, key) => { sounds.forEach((sampleInfoMap, key) => {
const value = Array.from(titlePathMap.keys()) const bank = Array.from(sampleInfoMap.keys())
.sort((a, b) => { .sort((a, b) => {
return a.localeCompare(b); return a.localeCompare(b);
}) })
.map((title) => titlePathMap.get(title)); .map((title) => sampleInfoMap.get(title));
registerSampleSource(key, bank, { prebake: false });
registerSampleSource(key, value, { prebake: false });
}); });
logger('imported sounds registered!', 'success'); logger('imported sounds registered!', 'success');