1import { Pattern } from '@strudel/core'; 2import { connectToDestination, getAudioContext, getWorklet } from 'superdough'; 3 4let doughWorklet; 5 6function initDoughWorklet() { 7 const ac = getAudioContext(); 8 doughWorklet = getWorklet( 9 ac, 10 'dough-processor', 11 {}, 12 { 13 outputChannelCount: [2], 14 }, 15 ); 16 connectToDestination(doughWorklet); // channels? 17} 18 19const soundMap = new Map(); 20const loadedSounds = new Map(); 21 22Pattern.prototype.supradough = function () { 23 return this.onTrigger((hap, __, cps, begin) => { 24 hap.value._begin = begin; 25 hap.value._duration = hap.duration / cps; 26 !doughWorklet && initDoughWorklet(); 27 const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s; 28 const n = hap.value.n ?? 0; 29 const soundKey = `${s}:${n}`; 30 if (soundMap.has(s)) { 31 hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers) 32 } 33 if (soundMap.has(s) && !loadedSounds.has(soundKey)) { 34 const urls = soundMap.get(s); 35 const url = urls[n % urls.length]; 36 console.log(`load ${soundKey} from ${url}`); 37 const loadSample = fetchSample(url); 38 loadedSounds.set(soundKey, loadSample); 39 loadSample.then(({ channels, sampleRate }) => 40 doughWorklet.port.postMessage({ 41 sample: soundKey, 42 channels, 43 sampleRate, 44 }), 45 ); 46 } 47 48 doughWorklet.port.postMessage({ spawn: hap.value }); 49 }, 1); 50}; 51 52function githubPath(base, subpath = '') { 53 if (!base.startsWith('github:')) { 54 throw new Error('expected "github:" at the start of pseudoUrl'); 55 } 56 let [_, path] = base.split('github:'); 57 path = path.endsWith('/') ? path.slice(0, -1) : path; 58 if (path.split('/').length === 2) { 59 // assume main as default branch if none set 60 path += '/main'; 61 } 62 return `https://raw.githubusercontent.com/${path}/${subpath}`; 63} 64export async function fetchSampleMap(url) { 65 if (url.startsWith('github:')) { 66 url = githubPath(url, 'strudel.json'); 67 } 68 if (url.startsWith('local:')) { 69 url = `http://localhost:5432`; 70 } 71 if (url.startsWith('shabda:')) { 72 let [_, path] = url.split('shabda:'); 73 url = `https://shabda.ndre.gr/${path}.json?strudel=1`; 74 } 75 if (url.startsWith('shabda/speech')) { 76 let [_, path] = url.split('shabda/speech'); 77 path = path.startsWith('/') ? path.substring(1) : path; 78 let [params, words] = path.split(':'); 79 let gender = 'f'; 80 let language = 'en-GB'; 81 if (params) { 82 [language, gender] = params.split('/'); 83 } 84 url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`; 85 } 86 if (typeof fetch !== 'function') { 87 // not a browser 88 return; 89 } 90 const base = url.split('/').slice(0, -1).join('/'); 91 if (typeof fetch === 'undefined') { 92 // skip fetch when in node / testing 93 return; 94 } 95 const json = await fetch(url) 96 .then((res) => res.json()) 97 .catch((error) => { 98 console.error(error); 99 throw new Error(`error loading "${url}"`); 100 }); 101 return [json, json._base || base]; 102}
for some reason, only piano and flute work.. is it because mp3??
106async function fetchSample(url) { 107 const buffer = await fetch(url) 108 .then((res) => res.arrayBuffer()) 109 .then((buf) => getAudioContext().decodeAudioData(buf)); 110 let channels = []; 111 for (let i = 0; i < buffer.numberOfChannels; i++) { 112 channels.push(buffer.getChannelData(i)); 113 } 114 return { channels, sampleRate: buffer.sampleRate }; 115}
117export async function supradoughsamples(sampleMap, baseUrl) { 118 if (typeof sampleMap === 'string') { 119 const [json, base] = await fetchSampleMap(sampleMap); 120 // console.log('json', json, 'base', base); 121 return supradoughsamples(json, base); 122 } 123 Object.entries(sampleMap).map(async ([key, urls]) => { 124 if (key !== '_base') { 125 urls = urls.map((url) => baseUrl + url); 126 // console.log('set', key, urls); 127 soundMap.set(key, urls); 128 } 129 }); 130}