1import { 2 processSampleMap, 3 registerSamplesPrefix, 4 registerSound, 5 onTriggerSample, 6 getAudioContext, 7 loadBuffer, 8} from '@strudel/webaudio'; 9 10let TAURI; 11if (typeof window !== 'undefined') { 12 TAURI = window?.__TAURI__; 13} 14export const { BaseDirectory, readDir, readBinaryFile, writeTextFile, readTextFile, exists } = TAURI?.fs || {}; 15 16export const dir = BaseDirectory?.Audio; // https://tauri.app/v1/api/js/path#audiodir 17const prefix = '~/music/'; 18 19async function hasStrudelJson(subpath) { 20 return exists(subpath + '/strudel.json', { dir }); 21} 22 23async function loadStrudelJson(subpath) { 24 const contents = await readTextFile(subpath + '/strudel.json', { dir }); 25 const sampleMap = JSON.parse(contents); 26 processSampleMap(sampleMap, (key, bank) => { 27 registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank, fileResolver(subpath)), { 28 type: 'sample', 29 samples: bank, 30 fileSystem: true, 31 tag: 'local', 32 }); 33 }); 34} 35 36async function writeStrudelJson(subpath) { 37 const children = await readDir(subpath, { dir, recursive: true }); 38 const name = subpath.split('/').slice(-1)[0]; 39 const tree = { name, children }; 40 41 let samples = {}; 42 let count = 0; 43 walkFileTree(tree, (entry, parent) => { 44 if (['wav', 'mp3'].includes(entry.name.split('.').slice(-1)[0])) { 45 samples[parent.name] = samples[parent.name] || []; 46 count += 1; 47 samples[parent.name].push(entry.subpath.slice(1).concat([entry.name]).join('/')); 48 } 49 }); 50 const json = JSON.stringify(samples, null, 2); 51 const filepath = subpath + '/strudel.json'; 52 await writeTextFile(filepath, json, { dir }); 53 console.log(`wrote strudel.json with ${count} samples to ${subpath}!`); 54} 55 56registerSamplesPrefix(prefix, async (path) => { 57 const subpath = path.replace(prefix, ''); 58 const hasJson = await hasStrudelJson(subpath); 59 if (!hasJson) { 60 await writeStrudelJson(subpath); 61 } 62 return loadStrudelJson(subpath); 63}); 64 65export const walkFileTree = (node, fn) => { 66 if (!Array.isArray(node?.children)) { 67 return; 68 } 69 for (const entry of node.children) { 70 entry.subpath = (node.subpath || []).concat([node.name]); 71 fn(entry, node); 72 if (entry.children) { 73 walkFileTree(entry, fn); 74 } 75 } 76}; 77 78export const isAudioFile = (filename) => 79 ['wav', 'mp3', 'flac', 'ogg', 'm4a', 'aac'].includes(filename.split('.').slice(-1)[0]); 80 81function uint8ArrayToDataURL(uint8Array) { 82 const blob = new Blob([uint8Array], { type: 'audio/*' }); 83 const dataURL = URL.createObjectURL(blob); 84 return dataURL; 85} 86 87const loadCache = {}; // caches local urls to data urls 88export async function resolveFileURL(url) { 89 if (loadCache[url]) { 90 return loadCache[url]; 91 } 92 loadCache[url] = (async () => { 93 const contents = await readBinaryFile(url, { dir }); 94 return uint8ArrayToDataURL(contents); 95 })(); 96 return loadCache[url]; 97} 98 99const fileResolver = (subpath) => (url) => resolveFileURL(subpath.endsWith('/') ? subpath + url : subpath + '/' + url); 100 101export async function playFile(path) { 102 const url = await resolveFileURL(path); 103 const ac = getAudioContext(); 104 const bufferSource = ac.createBufferSource(); 105 bufferSource.buffer = await loadBuffer(url, ac); 106 bufferSource.connect(ac.destination); 107 bufferSource.start(ac.currentTime); 108}