-
-
Notifications
You must be signed in to change notification settings - Fork 122
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #621 from tidalcycles/tauri-fs
desktop: play samples from disk
- Loading branch information
Showing
7 changed files
with
259 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div className="px-4 flex flex-col h-full"> | ||
<div className="flex justify-between font-mono pb-1"> | ||
<div> | ||
<span>{`samples('`}</span> | ||
{path?.map((p, i) => { | ||
if (i < path.length - 1) { | ||
return ( | ||
<Fragment key={i}> | ||
<span className="cursor-pointer underline" onClick={() => setPath((p) => p.slice(0, i + 1))}> | ||
{p.name} | ||
</span> | ||
<span>/</span> | ||
</Fragment> | ||
); | ||
} else { | ||
return ( | ||
<span className="cursor-pointer underline" key={i}> | ||
{p.name} | ||
</span> | ||
); | ||
} | ||
})} | ||
<span>{`')`}</span> | ||
</div> | ||
</div> | ||
<div className="overflow-auto"> | ||
{!folders?.length && !files?.length && <span className="text-gray-500">Nothing here</span>} | ||
{folders?.map((e, i) => ( | ||
<div className="cursor-pointer" key={i} onClick={() => select(e)}> | ||
{e.name} | ||
</div> | ||
))} | ||
{files?.map((e, i) => ( | ||
<div | ||
className="text-gray-500 cursor-pointer select-none" | ||
key={i} | ||
onClick={async () => playFile(`${subpath}/${e.name}`)} | ||
> | ||
{e.name} | ||
</div> | ||
))} | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { | ||
processSampleMap, | ||
registerSamplesPrefix, | ||
registerSound, | ||
onTriggerSample, | ||
getAudioContext, | ||
loadBuffer, | ||
} from '@strudel.cycles/webaudio'; | ||
|
||
let TAURI; | ||
if (typeof window !== 'undefined') { | ||
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters