jevstrudel.git / website / src / jev / sounds.mjs

The sounds tab's sounds as data: which of its sub-tabs a sound is under, how many variants it has, and how a song plays it. The sounds tab (repl/components/panel/SoundsTab.jsx) filters with inCategory, and the MCP's list_sounds and ask_jev_sound (tabTools.mjs) read the same, so what an AI is told is what the listener sees.

A sound is an entry of superdough's soundMap: [name, { data }], where data says its type (sample, synth, soundfont, wavetable), whether the REPL loaded it before any song (prebake), a tag ('drum-machines') and its files (samples, fonts, tables).

The sub-tabs, as the MCP names them, and the settings value each one is (settings.mjs's soundFilterType); imported is the part of user the import-sounds button added from disk (idbutils.mjs), whose files are blob: URLs of this browser.

16export const CATEGORIES = ['samples', 'drum-machines', 'synths', 'wavetables', 'user', 'imported'];
17export const FILTER_OF = { samples: 'samples', 'drum-machines': 'drums', synths: 'synths', wavetables: 'wavetables', user: 'user' };
19const firstUrl = (samples) => {
20  if (Array.isArray(samples)) return samples[0];
21  if (samples && typeof samples === 'object') {
22    const first = Object.values(samples)[0];
23    return Array.isArray(first) ? first[0] : first;
24  }
25  return typeof samples === 'string' ? samples : undefined;
26};
27const imported = (data) => !data?.prebake && data?.type === 'sample' && String(firstUrl(data.samples) ?? '').startsWith('blob:');

Whether a sound is under category (a sub-tab's filter value from soundFilterType, or an MCP category name); 'all' and undefined take all.

31export function inCategory(data, category) {
32  switch (category) {
33    case 'user':
34      return !data?.prebake;
35    case 'imported':
36      return imported(data);
37    case 'drums':
38    case 'drum-machines':
39      return data?.type === 'sample' && data.tag === 'drum-machines';
40    case 'samples':
41      return data?.type === 'sample' && data.tag !== 'drum-machines';
42    case 'synths':
43      return ['synth', 'soundfont'].includes(data?.type);
44    case 'wavetables':
45      return data?.type === 'wavetable';
46    default:
47      return true;
48  }
49}

The one sub-tab a sound shows under, besides user (which cuts across).

52export function categoryOf(data) {
53  if (imported(data)) return 'imported';
54  for (const c of ['drum-machines', 'samples', 'synths', 'wavetables']) if (inCategory(data, c)) return c;
55  return 'other';
56}
58const count = (x) => (Array.isArray(x) ? x.length : x && typeof x === 'object' ? Object.values(x).length : x ? 1 : 0);

How many variants a sound has, as the tab shows in brackets: a sample's files (chosen with .n()), a wavetable's tables, a soundfont's fonts.

62export function variantsOf(data) {
63  if (data?.type === 'sample') return count(data.samples);
64  if (data?.type === 'wavetable') return count(data.tables);
65  if (data?.type === 'soundfont') return count(data.fonts);
66  return 0;
67}
69const quote = (s) => JSON.stringify(s);

How a song plays it (its variants are .n(0), .n(1), …). A drum machine's sounds are named <machine>_<sound>, played as the sound with the machine as its bank.

74export function useOf(name, data) {
75  if (data?.type === 'synth' || data?.type === 'soundfont' || data?.type === 'wavetable') {
76    return `note("c3 e3 g3").s(${quote(name)})`;
77  }
78  const bank = data?.tag === 'drum-machines' ? /^(.+)_([^_]+)$/.exec(name) : null;
79  if (bank) return `s(${quote(bank[2])}).bank(${quote(bank[1])})`;
80  return `s(${quote(name)})`;
81}

The sounds a list shows: named, not internal (_…), in category, whose name contains filter (case aside), in name order.

85export function soundList(map, { category, filter = '' } = {}) {
86  const f = String(filter).toLowerCase();
87  return Object.entries(map ?? {})
88    .filter(([name, sound]) => !name.startsWith('_') && inCategory(sound?.data, category))
89    .filter(([name]) => name.toLowerCase().includes(f))
90    .sort((a, b) => a[0].localeCompare(b[0]));
91}

One sound as the MCP describes it.

94export function soundEntry(name, data) {
95  return { name, category: categoryOf(data), type: String(data?.type ?? ''), variants: variantsOf(data), use: useOf(name, data) };
96}