Connor McCutcheon
/ Music
supradough.mjs
mjs
import { Pattern } from '@strudel/core';
import { connectToDestination, getAudioContext, getWorklet } from 'superdough';
let doughWorklet;
function initDoughWorklet() {
  const ac = getAudioContext();
  doughWorklet = getWorklet(
    ac,
    'dough-processor',
    {},
    {
      outputChannelCount: [2],
    },
  );
  connectToDestination(doughWorklet); // channels?
}
const soundMap = new Map();
const loadedSounds = new Map();
Pattern.prototype.supradough = function () {
  return this.onTrigger((hap, __, cps, begin) => {
    hap.value._begin = begin;
    hap.value._duration = hap.duration / cps;
    !doughWorklet && initDoughWorklet();
    const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s;
    const n = hap.value.n ?? 0;
    const soundKey = `${s}:${n}`;
    if (soundMap.has(s)) {
      hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers)
    }
    if (soundMap.has(s) && !loadedSounds.has(soundKey)) {
      const urls = soundMap.get(s);
      const url = urls[n % urls.length];
      console.log(`load ${soundKey} from ${url}`);
      const loadSample = fetchSample(url);
      loadedSounds.set(soundKey, loadSample);
      loadSample.then(({ channels, sampleRate }) =>
        doughWorklet.port.postMessage({
          sample: soundKey,
          channels,
          sampleRate,
        }),
      );
    }
    doughWorklet.port.postMessage({ spawn: hap.value });
  }, 1);
};
function githubPath(base, subpath = '') {
  if (!base.startsWith('github:')) {
    throw new Error('expected "github:" at the start of pseudoUrl');
  }
  let [_, path] = base.split('github:');
  path = path.endsWith('/') ? path.slice(0, -1) : path;
  if (path.split('/').length === 2) {
    // assume main as default branch if none set
    path += '/main';
  }
  return `https://raw.githubusercontent.com/${path}/${subpath}`;
}
export async function fetchSampleMap(url) {
  if (url.startsWith('github:')) {
    url = githubPath(url, 'strudel.json');
  }
  if (url.startsWith('local:')) {
    url = `http://localhost:5432`;
  }
  if (url.startsWith('shabda:')) {
    let [_, path] = url.split('shabda:');
    url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
  }
  if (url.startsWith('shabda/speech')) {
    let [_, path] = url.split('shabda/speech');
    path = path.startsWith('/') ? path.substring(1) : path;
    let [params, words] = path.split(':');
    let gender = 'f';
    let language = 'en-GB';
    if (params) {
      [language, gender] = params.split('/');
    }
    url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
  }
  if (typeof fetch !== 'function') {
    // not a browser
    return;
  }
  const base = url.split('/').slice(0, -1).join('/');
  if (typeof fetch === 'undefined') {
    // skip fetch when in node / testing
    return;
  }
  const json = await fetch(url)
    .then((res) => res.json())
    .catch((error) => {
      console.error(error);
      throw new Error(`error loading "${url}"`);
    });
  return [json, json._base || base];
}
// for some reason, only piano and flute work.. is it because mp3??
async function fetchSample(url) {
  const buffer = await fetch(url)
    .then((res) => res.arrayBuffer())
    .then((buf) => getAudioContext().decodeAudioData(buf));
  let channels = [];
  for (let i = 0; i < buffer.numberOfChannels; i++) {
    channels.push(buffer.getChannelData(i));
  }
  return { channels, sampleRate: buffer.sampleRate };
}
export async function doughsamples(sampleMap, baseUrl) {
  if (typeof sampleMap === 'string') {
    const [json, base] = await fetchSampleMap(sampleMap);
    // console.log('json', json, 'base', base);
    return doughsamples(json, base);
  }
  Object.entries(sampleMap).map(async ([key, urls]) => {
    if (key !== '_base') {
      urls = urls.map((url) => baseUrl + url);
      // console.log('set', key, urls);
      soundMap.set(key, urls);
    }
  });
}
No comments yet.