diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 429415c8..bbd30e26 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -16,7 +16,7 @@ "all": false, "fs": { "all": true, - "scope": ["$AUDIO/**", "$AUDIO"] + "scope": ["$HOME/**", "$HOME", "$HOME/*"] } }, "bundle": { diff --git a/website/src/repl/FilesTab.jsx b/website/src/repl/FilesTab.jsx index ed8d1215..87b40584 100644 --- a/website/src/repl/FilesTab.jsx +++ b/website/src/repl/FilesTab.jsx @@ -1,59 +1,21 @@ import { Fragment, useEffect } from 'react'; -import { getAudioContext, loadBuffer, processSampleMap } from '@strudel.cycles/webaudio'; import React, { useMemo, useState } from 'react'; - -const TAURI = window.__TAURI__; -const { BaseDirectory, readDir, readBinaryFile, writeTextFile, readTextFile } = TAURI?.fs || {}; - -async function loadFiles() { - return readDir('', { dir: BaseDirectory.Audio, recursive: true }); -} - -const walkFileTree = (node, fn) => { - if (!Array.isArray(node?.children)) { - return; - } - for (const entry of node.children) { - entry.subpath = (node.subpath || []).concat([node.name]); - fn(entry, node); - if (entry.children) { - walkFileTree(entry, fn); - } - } -}; - -const isAudioFile = (filename) => ['wav', 'mp3'].includes(filename.split('.').slice(-1)[0]); -function uint8ArrayToDataURL(uint8Array) { - const blob = new Blob([uint8Array], { type: 'audio/*' }); - const dataURL = URL.createObjectURL(blob); - return dataURL; -} - -const loadCache = {}; // caches local urls to data urls -async function resolveFileURL(url) { - if (loadCache[url]) { - return loadCache[url]; - } - loadCache[url] = (async () => { - const contents = await readBinaryFile(url, { - dir: BaseDirectory.Audio, - }); - return uint8ArrayToDataURL(contents); - })(); - return loadCache[url]; -} +import { isAudioFile, readDir, dir } from './files.mjs'; export function FilesTab() { const [path, setPath] = useState([]); useEffect(() => { let init = false; - loadFiles().then((_tree) => setPath([{ name: 'files', children: _tree, path: BaseDirectory.Audio }])); + readDir('', { dir, recursive: true }) + .then((children) => setPath([{ name: '~/music', children }])) + .catch((err) => { + console.log('error loadin files', err); + }); return () => { init = true; }; }, []); const current = useMemo(() => path[path.length - 1], [path]); - const strudelJson = useMemo(() => current?.children?.find((e) => e.name === 'strudel.json'), [current]); const subpath = useMemo( () => path @@ -63,47 +25,36 @@ export function FilesTab() { [path], ); const folders = useMemo(() => current?.children.filter((e) => !!e.children), [current]); - - const files = useMemo( - () => current?.children.filter((e) => !e.children && isAudioFile(e.name) /* || e.name === 'strudel.json' */), - [current], - ); - const audioFiles = useMemo(() => { - let files = []; - walkFileTree(current, (entry) => { - if (isAudioFile(entry.name)) { - files.push(entry); - } - }); - return files; - }, [current]); + const files = useMemo(() => current?.children.filter((e) => !e.children && isAudioFile(e.name)), [current]); const select = (e) => setPath((p) => p.concat([e])); - return (
-
+
+ {`samples('`} {path?.map((p, i) => { if (i < path.length - 1) { return ( setPath((p) => p.slice(0, i + 1))}> {p.name} - {' '} - /{' '} + + / ); } else { return ( - - {p.name}{' '} + + {p.name} ); } })} + {`')`}
+ {!folders?.length && !files?.length && Nothing here} {folders?.map((e, i) => (
select(e)}> {e.name} @@ -113,75 +64,12 @@ export function FilesTab() {
{ - const url = await resolveFileURL(`${subpath}/${e.name}`); - const ac = getAudioContext(); - const bufferSource = ac.createBufferSource(); - bufferSource.buffer = await loadBuffer(url, ac); - bufferSource.connect(ac.destination); - bufferSource.start(ac.currentTime); - }} + onClick={async () => playFile(`${subpath}/${e.name}`)} > {e.name}
))}
- -
- - {strudelJson && ( - <> - - {/* */} - - )} -
); } diff --git a/website/src/repl/files.mjs b/website/src/repl/files.mjs new file mode 100644 index 00000000..35a538a7 --- /dev/null +++ b/website/src/repl/files.mjs @@ -0,0 +1,104 @@ +import { + processSampleMap, + registerSamplesPrefix, + registerSound, + onTriggerSample, + getAudioContext, + loadBuffer, +} from '@strudel.cycles/webaudio'; + +const TAURI = window.__TAURI__; +export const { BaseDirectory, readDir, readBinaryFile, writeTextFile, readTextFile, exists } = TAURI?.fs || {}; + +export const dir = BaseDirectory?.Audio; // https://tauri.app/v1/api/js/path#audiodir +const prefix = '~/music/'; + +async function hasStrudelJson(subpath) { + return exists(subpath + '/strudel.json', { dir }); +} + +async function loadStrudelJson(subpath) { + const contents = await readTextFile(subpath + '/strudel.json', { dir }); + const sampleMap = JSON.parse(contents); + processSampleMap(sampleMap, (key, value) => { + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value, fileResolver(subpath)), { + type: 'sample', + samples: value, + fileSystem: true, + tag: 'local', + }); + }); +} + +async function writeStrudelJson(subpath) { + const children = await readDir(subpath, { dir, recursive: true }); + const name = subpath.split('/').slice(-1)[0]; + const tree = { name, children }; + + let samples = {}; + let count = 0; + walkFileTree(tree, (entry, parent) => { + if (['wav', 'mp3'].includes(entry.name.split('.').slice(-1)[0])) { + samples[parent.name] = samples[parent.name] || []; + count += 1; + samples[parent.name].push(entry.subpath.slice(1).concat([entry.name]).join('/')); + } + }); + const json = JSON.stringify(samples, null, 2); + const filepath = subpath + '/strudel.json'; + await writeTextFile(filepath, json, { dir }); + console.log(`wrote strudel.json with ${count} samples to ${subpath}!`); +} + +registerSamplesPrefix(prefix, async (path) => { + const subpath = path.replace(prefix, ''); + const hasJson = await hasStrudelJson(subpath); + if (!hasJson) { + await writeStrudelJson(subpath); + } + return loadStrudelJson(subpath); +}); + +export const walkFileTree = (node, fn) => { + if (!Array.isArray(node?.children)) { + return; + } + for (const entry of node.children) { + entry.subpath = (node.subpath || []).concat([node.name]); + fn(entry, node); + if (entry.children) { + walkFileTree(entry, fn); + } + } +}; + +export const isAudioFile = (filename) => ['wav', 'mp3'].includes(filename.split('.').slice(-1)[0]); + +function uint8ArrayToDataURL(uint8Array) { + const blob = new Blob([uint8Array], { type: 'audio/*' }); + const dataURL = URL.createObjectURL(blob); + return dataURL; +} + +const loadCache = {}; // caches local urls to data urls +export async function resolveFileURL(url) { + if (loadCache[url]) { + return loadCache[url]; + } + loadCache[url] = (async () => { + const contents = await readBinaryFile(url, { dir }); + return uint8ArrayToDataURL(contents); + })(); + return loadCache[url]; +} + +const fileResolver = (subpath) => (url) => resolveFileURL(subpath.endsWith('/') ? subpath + url : subpath + '/' + url); + +export async function playFile(path) { + const url = await resolveFileURL(path); + const ac = getAudioContext(); + const bufferSource = ac.createBufferSource(); + bufferSource.buffer = await loadBuffer(url, ac); + bufferSource.connect(ac.destination); + bufferSource.start(ac.currentTime); +} diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index ec50de0d..6bdc5c0d 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -1,6 +1,7 @@ import { Pattern, noteToMidi, valueToMidi } from '@strudel.cycles/core'; import { registerSynthSounds, samples } from '@strudel.cycles/webaudio'; import './piano.mjs'; +import './files.mjs'; export async function prebake() { // https://archive.org/details/SalamanderGrandPianoV3