mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 21:58:31 +00:00
Merge pull request #839 from daslyfe/file_import
Sound Import from local file system
This commit is contained in:
commit
daf9187572
@ -33,6 +33,7 @@ import { code2hash, hash2code } from './helpers.mjs';
|
||||
import { isTauri } from '../tauri.mjs';
|
||||
import { useWidgets } from '@strudel.cycles/react/src/hooks/useWidgets.mjs';
|
||||
import { writeText } from '@tauri-apps/api/clipboard';
|
||||
import { registerSamplesFromDB, userSamplesDBConfig } from './idbutils.mjs';
|
||||
|
||||
const { latestCode } = settingsMap.get();
|
||||
|
||||
@ -194,6 +195,8 @@ export function Repl({ embedded = false }) {
|
||||
setCode(randomTune);
|
||||
msg = `A random code snippet named "${name}" has been loaded!`;
|
||||
}
|
||||
//registers samples that have been saved to the index DB
|
||||
registerSamplesFromDB(userSamplesDBConfig);
|
||||
logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
|
||||
setPending(false);
|
||||
});
|
||||
|
||||
148
website/src/repl/idbutils.mjs
Normal file
148
website/src/repl/idbutils.mjs
Normal file
@ -0,0 +1,148 @@
|
||||
import { registerSound, onTriggerSample } from '@strudel.cycles/webaudio';
|
||||
import { isAudioFile } from './files.mjs';
|
||||
import { logger } from '@strudel.cycles/core';
|
||||
|
||||
//utilites for writing and reading to the indexdb
|
||||
|
||||
export const userSamplesDBConfig = {
|
||||
dbName: 'samples',
|
||||
table: 'usersamples',
|
||||
columns: ['blob', 'title'],
|
||||
version: 1,
|
||||
};
|
||||
|
||||
// deletes all of the databases, useful for debugging
|
||||
const clearIDB = () => {
|
||||
window.indexedDB
|
||||
.databases()
|
||||
.then((r) => {
|
||||
for (var i = 0; i < r.length; i++) window.indexedDB.deleteDatabase(r[i].name);
|
||||
})
|
||||
.then(() => {
|
||||
alert('All data cleared.');
|
||||
});
|
||||
};
|
||||
|
||||
// queries the DB, and registers the sounds so they can be played
|
||||
export const registerSamplesFromDB = (config, onComplete = () => {}) => {
|
||||
openDB(config, (objectStore) => {
|
||||
let query = objectStore.getAll();
|
||||
query.onsuccess = (event) => {
|
||||
const soundFiles = event.target.result;
|
||||
if (!soundFiles?.length) {
|
||||
return;
|
||||
}
|
||||
const sounds = new Map();
|
||||
[...soundFiles]
|
||||
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
|
||||
.forEach((soundFile) => {
|
||||
const title = soundFile.title;
|
||||
if (!isAudioFile(title)) {
|
||||
return;
|
||||
}
|
||||
const splitRelativePath = soundFile.id?.split('/');
|
||||
const parentDirectory = splitRelativePath[splitRelativePath.length - 2];
|
||||
const soundPath = soundFile.blob;
|
||||
const soundPaths = sounds.get(parentDirectory) ?? new Set();
|
||||
soundPaths.add(soundPath);
|
||||
sounds.set(parentDirectory, soundPaths);
|
||||
});
|
||||
|
||||
sounds.forEach((soundPaths, key) => {
|
||||
const value = Array.from(soundPaths);
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
|
||||
type: 'sample',
|
||||
samples: value,
|
||||
baseUrl: undefined,
|
||||
prebake: false,
|
||||
tag: undefined,
|
||||
});
|
||||
});
|
||||
logger('imported sounds registered!', 'success');
|
||||
onComplete();
|
||||
};
|
||||
});
|
||||
};
|
||||
// creates a blob from a buffer that can be read
|
||||
async function bufferToDataUrl(buf) {
|
||||
return new Promise((resolve) => {
|
||||
var blob = new Blob([buf], { type: 'application/octet-binary' });
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
resolve(event.target.result);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
//open db and initialize it if necessary
|
||||
const openDB = (config, onOpened) => {
|
||||
const { dbName, version, table, columns } = config;
|
||||
if (!('indexedDB' in window)) {
|
||||
console.log('IndexedDB is not supported.');
|
||||
return;
|
||||
}
|
||||
const dbOpen = indexedDB.open(dbName, version);
|
||||
|
||||
dbOpen.onupgradeneeded = (_event) => {
|
||||
const db = dbOpen.result;
|
||||
const objectStore = db.createObjectStore(table, { keyPath: 'id', autoIncrement: false });
|
||||
columns.forEach((c) => {
|
||||
objectStore.createIndex(c, c, { unique: false });
|
||||
});
|
||||
};
|
||||
dbOpen.onerror = (err) => {
|
||||
logger('Something went wrong while trying to open the the client DB', 'error');
|
||||
console.error(`indexedDB error: ${err.errorCode}`);
|
||||
};
|
||||
|
||||
dbOpen.onsuccess = () => {
|
||||
const db = dbOpen.result;
|
||||
|
||||
const // lock store for writing
|
||||
writeTransaction = db.transaction([table], 'readwrite'),
|
||||
// get object store
|
||||
objectStore = writeTransaction.objectStore(table);
|
||||
onOpened(objectStore, db);
|
||||
};
|
||||
};
|
||||
|
||||
const processFilesForIDB = async (files) => {
|
||||
return await Promise.all(
|
||||
Array.from(files)
|
||||
.map(async (s) => {
|
||||
const title = s.name;
|
||||
if (!isAudioFile(title)) {
|
||||
return;
|
||||
}
|
||||
//create obscured url to file system that can be fetched
|
||||
const sUrl = URL.createObjectURL(s);
|
||||
//fetch the sound and turn it into a buffer array
|
||||
const buf = await fetch(sUrl).then((res) => res.arrayBuffer());
|
||||
//create a url blob containing all of the buffer data
|
||||
const base64 = await bufferToDataUrl(buf);
|
||||
return {
|
||||
title,
|
||||
blob: base64,
|
||||
id: s.webkitRelativePath,
|
||||
};
|
||||
})
|
||||
.filter(Boolean),
|
||||
).catch((error) => {
|
||||
logger('Something went wrong while processing uploaded files', 'error');
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
export const uploadSamplesToDB = async (config, files) => {
|
||||
await processFilesForIDB(files).then((files) => {
|
||||
const onOpened = (objectStore, _db) => {
|
||||
files.forEach((file) => {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
objectStore.put(file);
|
||||
});
|
||||
};
|
||||
openDB(config, onOpened);
|
||||
});
|
||||
};
|
||||
@ -9,7 +9,7 @@ export function ButtonGroup({ value, onChange, items }) {
|
||||
key={key}
|
||||
onClick={() => onChange(key)}
|
||||
className={cx(
|
||||
'px-2 border-b h-8',
|
||||
'px-2 border-b h-8 whitespace-nowrap',
|
||||
// i === 0 && 'rounded-l-md',
|
||||
// i === arr.length - 1 && 'rounded-r-md',
|
||||
// value === key ? 'bg-background' : 'bg-lineHighlight',
|
||||
|
||||
43
website/src/repl/panel/ImportSoundsButton.jsx
Normal file
43
website/src/repl/panel/ImportSoundsButton.jsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { registerSamplesFromDB, uploadSamplesToDB, userSamplesDBConfig } from '../idbutils.mjs';
|
||||
|
||||
//choose a directory to locally import samples
|
||||
export default function ImportSoundsButton({ onComplete }) {
|
||||
let fileUploadRef = React.createRef();
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const onChange = useCallback(async () => {
|
||||
if (!fileUploadRef.current.files?.length) {
|
||||
return;
|
||||
}
|
||||
setIsUploading(true);
|
||||
await uploadSamplesToDB(userSamplesDBConfig, fileUploadRef.current.files).then(() => {
|
||||
registerSamplesFromDB(userSamplesDBConfig, () => {
|
||||
onComplete();
|
||||
setIsUploading(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<label
|
||||
style={{ alignItems: 'center' }}
|
||||
className="flex bg-background ml-2 pl-2 pr-2 max-w-[300px] rounded-md hover:opacity-50 whitespace-nowrap cursor-pointer"
|
||||
>
|
||||
<input
|
||||
disabled={isUploading}
|
||||
ref={fileUploadRef}
|
||||
id="audio_file"
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
directory=""
|
||||
webkitdirectory=""
|
||||
multiple
|
||||
accept="audio/*"
|
||||
onChange={() => {
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
{isUploading ? 'importing...' : 'import sounds'}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { getAudioContext, soundMap, connectToDestination } from '@strudel.cycles
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { settingsMap, useSettings } from '../../settings.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
||||
|
||||
const getSamples = (samples) =>
|
||||
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
||||
@ -43,7 +44,7 @@ export function SoundsTab() {
|
||||
});
|
||||
return (
|
||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
|
||||
<div className="pb-2 flex-none">
|
||||
<div className="pb-2 flex shrink-0 overflow-auto">
|
||||
<ButtonGroup
|
||||
value={soundsFilter}
|
||||
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
|
||||
@ -54,6 +55,7 @@ export function SoundsTab() {
|
||||
user: 'User',
|
||||
}}
|
||||
></ButtonGroup>
|
||||
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
|
||||
</div>
|
||||
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal">
|
||||
{soundEntries.map(([name, { data, onTrigger }]) => (
|
||||
|
||||
@ -114,6 +114,9 @@ export async function prebake() {
|
||||
],
|
||||
},
|
||||
'github:tidalcycles/Dirt-Samples/master/',
|
||||
{
|
||||
prebake: true,
|
||||
},
|
||||
),
|
||||
]);
|
||||
// await samples('github:tidalcycles/Dirt-Samples/master');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user