jevstrudel.git / website / src / repl / files.mjs
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}