1import { logger } from './logger.mjs'; 2 3// currently duplicate with core util.mjs to skip dependency 4// TODO: add separate util module? 5 6export const tokenizeNote = (note) => { 7 if (typeof note !== 'string') { 8 return []; 9 } 10 const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || []; 11 if (!pc) { 12 return []; 13 } 14 return [pc, acc, oct ? Number(oct) : undefined]; 15}; 16const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }; 17const accs = { '#': 1, b: -1, s: 1, f: -1 }; 18 19export const getAccidentalsOffset = (accidentals) => { 20 return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0; 21}; 22 23export const noteToMidi = (note, defaultOctave = 3) => { 24 const [pc, acc, oct = defaultOctave] = tokenizeNote(note); 25 if (!pc) { 26 throw new Error('not a note: "' + note + '"'); 27 } 28 const chroma = chromas[pc.toLowerCase()]; 29 const offset = getAccidentalsOffset(acc); 30 return (Number(oct) + 1) * 12 + chroma + offset; 31}; 32export const midiToFreq = (n) => { 33 return Math.pow(2, (n - 69) / 12) * 440; 34}; 35export const clamp = (num, min, max) => Math.min(Math.max(num, min), max); 36 37export const freqToMidi = (freq) => { 38 return (12 * Math.log(freq / 440)) / Math.LN2 + 69; 39}; 40 41export const valueToMidi = (value, fallbackValue) => { 42 if (typeof value !== 'object') { 43 throw new Error('valueToMidi: expected object value'); 44 } 45 let { freq, note } = value; 46 if (typeof freq === 'number') { 47 return freqToMidi(freq); 48 } 49 if (typeof note === 'string') { 50 return noteToMidi(note); 51 } 52 if (typeof note === 'number') { 53 return note; 54 } 55 if (!fallbackValue) { 56 throw new Error('valueToMidi: expected freq or note to be set'); 57 } 58 return fallbackValue; 59}; 60 61export function nanFallback(value, fallback = 0, silent) { 62 if (isNaN(Number(value))) { 63 !silent && logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning'); 64 return fallback; 65 } 66 return value; 67} 68// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does) 69export const _mod = (n, m) => ((n % m) + m) % m; 70 71// round to nearest int, negative numbers will output a subtracted index 72export const getSoundIndex = (n, numSounds) => { 73 return _mod(Math.round(nanFallback(n, 0)), numSounds); 74}; 75 76export function cycleToSeconds(cycle, cps) { 77 return cycle / cps; 78} 79 80export function secondsToCycle(t, cps) { 81 return t * cps; 82} 83 84// deduces relevant info for sample loading from hap.value and sample definition 85// it encapsulates the core sampler logic into a pure and synchronous function 86// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format) 87export function getCommonSampleInfo(hapValue, bank) { 88 const { s, n = 0 } = hapValue; 89 let midi = valueToMidi(hapValue, 36); 90 let transpose = midi - 36; // C3 is middle C; 91 let url; 92 let index = 0; 93 if (Array.isArray(bank)) { 94 index = getSoundIndex(n, bank.length); 95 url = bank[index]; 96 } else { 97 const midiDiff = (noteA) => noteToMidi(noteA) - midi; 98 // object format will expect keys as notes 99 const closest = Object.keys(bank) 100 .filter((k) => !k.startsWith('_')) 101 .reduce( 102 (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), 103 null, 104 ); 105 transpose = -midiDiff(closest); // semitones to repitch 106 index = getSoundIndex(n, bank[closest].length); 107 url = bank[closest][index]; 108 } 109 const label = `${s}:${index}`; 110 return { transpose, url, index, midi, label }; 111} 112 113/** Selects entries from `source` and renames them via `map` 114 * @tags internals 115 */ 116export const pickAndRename = (source, map) => { 117 return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]])); 118}; 119 120export const getBaseURL = (url) => { 121 try { 122 // For real URLs 123 return new URL('.', new URL(url)).href.replace(/\/$/, ''); // removes trailing slash 124 } catch { 125 // For pseudo URLS 126 return url.split('/').slice(0, -1).join('/'); 127 } 128};