util.mjsannotatedutil.mjssource128 lines · 4.0 KB · raw
1import { logger } from './logger.mjs';

currently duplicate with core util.mjs to skip dependency TODO: add separate util module?

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 };
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}

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;

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};
76export function cycleToSeconds(cycle, cps) {
77  return cycle / cps;
78}
79
80export function secondsToCycle(t, cps) {
81  return t * cps;
82}

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)

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}

Selects entries from source and renames them via map @tags internals

116export const pickAndRename = (source, map) => {
117  return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
118};
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};