diff --git a/packages/webaudio/sampler.mjs b/packages/webaudio/sampler.mjs index 449b5e39..17275a99 100644 --- a/packages/webaudio/sampler.mjs +++ b/packages/webaudio/sampler.mjs @@ -21,7 +21,7 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -export const getSampleBufferSource = async (s, n, note, speed, freq, bank) => { +export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resolveUrl) => { let transpose = 0; if (freq !== undefined && note !== undefined) { logger('[sampler] hap has note and freq. ignoring note', 'warning'); @@ -45,6 +45,9 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank) => { transpose = -midiDiff(closest); // semitones to repitch sampleUrl = bank[closest][n % bank[closest].length]; } + if (resolveUrl) { + sampleUrl = await resolveUrl(sampleUrl); + } let buffer = await loadBuffer(sampleUrl, ac, s, n); if (speed < 0) { // should this be cached? @@ -91,6 +94,46 @@ export const getLoadedBuffer = (url) => { return bufferCache[url]; }; +export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => { + return Object.entries(sampleMap).forEach(([key, value]) => { + if (typeof value === 'string') { + value = [value]; + } + if (typeof value !== 'object') { + throw new Error('wrong sample map format for ' + key); + } + baseUrl = value._base || baseUrl; + const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/'); + if (Array.isArray(value)) { + //return [key, value.map(replaceUrl)]; + value = value.map(replaceUrl); + } else { + // must be object + value = Object.fromEntries( + Object.entries(value).map(([note, samples]) => { + return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)]; + }), + ); + } + fn(key, value); + }); +}; + +// allows adding a custom url prefix handler +// for example, it is used by the desktop app to load samples starting with '~/music' +let resourcePrefixHandlers = {}; +export function registerSamplesPrefix(prefix, resolve) { + resourcePrefixHandlers[prefix] = resolve; +} +// finds a prefix handler for the given url (if any) +function getSamplesPrefixHandler(url) { + const handler = Object.entries(resourcePrefixHandlers).find(([key]) => url.startsWith(key)); + if (handler) { + return handler[1]; + } + return; +} + /** * Loads a collection of samples to use with `s` * @example @@ -107,6 +150,11 @@ export const getLoadedBuffer = (url) => { export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => { if (typeof sampleMap === 'string') { + // check if custom prefix handler + const handler = getSamplesPrefixHandler(sampleMap); + if (handler) { + return handler(sampleMap); + } if (sampleMap.startsWith('github:')) { let [_, path] = sampleMap.split('github:'); path = path.endsWith('/') ? path.slice(0, -1) : path; @@ -130,39 +178,23 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option }); } const { prebake, tag } = options; - Object.entries(sampleMap).forEach(([key, value]) => { - if (typeof value === 'string') { - value = [value]; - } - if (typeof value !== 'object') { - throw new Error('wrong sample map format for ' + key); - } - baseUrl = value._base || baseUrl; - const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/'); - if (Array.isArray(value)) { - //return [key, value.map(replaceUrl)]; - value = value.map(replaceUrl); - } else { - // must be object - value = Object.fromEntries( - Object.entries(value).map(([note, samples]) => { - return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)]; - }), - ); - } - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { - type: 'sample', - samples: value, - baseUrl, - prebake, - tag, - }); - }); + processSampleMap( + sampleMap, + (key, value) => + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { + type: 'sample', + samples: value, + baseUrl, + prebake, + tag, + }), + baseUrl, + ); }; const cutGroups = []; -export async function onTriggerSample(t, value, onended, bank) { +export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const { s, freq, @@ -188,7 +220,7 @@ export async function onTriggerSample(t, value, onended, bank) { //const soundfont = getSoundfontKey(s); const time = t + nudge; - const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank); + const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl); // asny stuff above took too long? if (ac.currentTime > t) { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 30ad48de..b7401f85 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,7 +17,7 @@ tauri-build = { version = "1.4.0", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.4.0", features = [] } +tauri = { version = "1.4.0", features = ["fs-all"] } [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5392007f..bbd30e26 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -4,7 +4,8 @@ "beforeBuildCommand": "npm run build", "beforeDevCommand": "npm run dev", "devPath": "http://localhost:3000", - "distDir": "../website/dist" + "distDir": "../website/dist", + "withGlobalTauri": true }, "package": { "productName": "Strudel", @@ -12,7 +13,11 @@ }, "tauri": { "allowlist": { - "all": false + "all": false, + "fs": { + "all": true, + "scope": ["$HOME/**", "$HOME", "$HOME/*"] + } }, "bundle": { "active": true, diff --git a/website/src/repl/FilesTab.jsx b/website/src/repl/FilesTab.jsx new file mode 100644 index 00000000..e04086b6 --- /dev/null +++ b/website/src/repl/FilesTab.jsx @@ -0,0 +1,75 @@ +import { Fragment, useEffect } from 'react'; +import React, { useMemo, useState } from 'react'; +import { isAudioFile, readDir, dir, playFile } from './files.mjs'; + +export function FilesTab() { + const [path, setPath] = useState([]); + useEffect(() => { + let init = false; + 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 subpath = useMemo( + () => + path + .slice(1) + .map((p) => p.name) + .join('/'), + [path], + ); + const folders = useMemo(() => current?.children.filter((e) => !!e.children), [current]); + const files = useMemo(() => current?.children.filter((e) => !e.children && isAudioFile(e.name)), [current]); + const select = (e) => setPath((p) => p.concat([e])); + return ( +