1// The sounds tab's sounds as data: which of its sub-tabs a sound is under, 2// how many variants it has, and how a song plays it. The sounds tab 3// (repl/components/panel/SoundsTab.jsx) filters with `inCategory`, and the 4// MCP's list_sounds and ask_jev_sound (tabTools.mjs) read the same, so 5// what an AI is told is what the listener sees. 6// 7// A sound is an entry of superdough's soundMap: `[name, { data }]`, where 8// `data` says its `type` (sample, synth, soundfont, wavetable), whether the 9// REPL loaded it before any song (`prebake`), a `tag` ('drum-machines') and 10// its files (`samples`, `fonts`, `tables`). 11 12// The sub-tabs, as the MCP names them, and the settings value each one is 13// (settings.mjs's soundFilterType); `imported` is the part of `user` the 14// import-sounds button added from disk (idbutils.mjs), whose files are 15// 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' }; 18 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:'); 28 29// Whether a sound is under `category` (a sub-tab's filter value from 30// 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} 50 51// 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} 57 58const count = (x) => (Array.isArray(x) ? x.length : x && typeof x === 'object' ? Object.values(x).length : x ? 1 : 0); 59 60// How many variants a sound has, as the tab shows in brackets: a sample's 61// 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} 68 69const quote = (s) => JSON.stringify(s); 70 71// How a song plays it (its variants are .n(0), .n(1), …). A drum 72// machine's sounds are named `<machine>_<sound>`, played as the sound with 73// 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} 82 83// The sounds a list shows: named, not internal (`_…`), in `category`, 84// 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} 92 93// 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}