From 61919a1b3e87d5e66957351d11409130ffc6f0cd Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 3 Dec 2023 23:33:04 -0500 Subject: [PATCH 01/51] upload samples --- website/src/repl/FileUpload.jsx | 35 +++++++++++++++++++++++++++++++++ website/src/repl/Footer.jsx | 2 ++ 2 files changed, 37 insertions(+) create mode 100644 website/src/repl/FileUpload.jsx diff --git a/website/src/repl/FileUpload.jsx b/website/src/repl/FileUpload.jsx new file mode 100644 index 00000000..ab606657 --- /dev/null +++ b/website/src/repl/FileUpload.jsx @@ -0,0 +1,35 @@ +import React from 'react'; + +export default function FileUpload({ onUpload }) { + let fileUploadRef = React.createRef(); + function mapFiles(soundFiles) { + const files = Array.from(soundFiles).map((soundFile) => { + const file = { name: soundFile.name, path: URL.createObjectURL(soundFile) }; + return file; + }); + onUpload(files); + } + return ( + <> + { + mapFiles(fileUploadRef.current.files); + }} + /> + fileUploadRef.current.click()} + /> + + ); +} diff --git a/website/src/repl/Footer.jsx b/website/src/repl/Footer.jsx index b03f62d0..833a7a75 100644 --- a/website/src/repl/Footer.jsx +++ b/website/src/repl/Footer.jsx @@ -10,6 +10,7 @@ import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../s import { getAudioContext, soundMap } from '@strudel.cycles/webaudio'; import { useStore } from '@nanostores/react'; import { FilesTab } from './FilesTab'; +import FileUpload from './FileUpload'; const TAURI = window.__TAURI__; @@ -424,6 +425,7 @@ function SettingsTab({ scheduler }) { onChange={(fontFamily) => settingsMap.setKey('fontFamily', fontFamily)} /> + console.log(files)} /> Date: Mon, 4 Dec 2023 19:11:38 -0500 Subject: [PATCH 02/51] style input --- packages/superdough/sampler.mjs | 7 +-- website/src/repl/FileUpload.jsx | 35 --------------- website/src/repl/Footer.jsx | 6 +-- website/src/repl/ImportSoundsButton.jsx | 58 +++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 41 deletions(-) delete mode 100644 website/src/repl/FileUpload.jsx create mode 100644 website/src/repl/ImportSoundsButton.jsx diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index b8f10d5d..d3e6a5a4 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -209,14 +209,15 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const { prebake, tag } = options; processSampleMap( sampleMap, - (key, value) => - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { + (key, value) => { + return registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { type: 'sample', samples: value, baseUrl, prebake, tag, - }), + }); + }, baseUrl, ); }; diff --git a/website/src/repl/FileUpload.jsx b/website/src/repl/FileUpload.jsx deleted file mode 100644 index ab606657..00000000 --- a/website/src/repl/FileUpload.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; - -export default function FileUpload({ onUpload }) { - let fileUploadRef = React.createRef(); - function mapFiles(soundFiles) { - const files = Array.from(soundFiles).map((soundFile) => { - const file = { name: soundFile.name, path: URL.createObjectURL(soundFile) }; - return file; - }); - onUpload(files); - } - return ( - <> - { - mapFiles(fileUploadRef.current.files); - }} - /> - fileUploadRef.current.click()} - /> - - ); -} diff --git a/website/src/repl/Footer.jsx b/website/src/repl/Footer.jsx index 833a7a75..7854d25e 100644 --- a/website/src/repl/Footer.jsx +++ b/website/src/repl/Footer.jsx @@ -10,7 +10,7 @@ import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../s import { getAudioContext, soundMap } from '@strudel.cycles/webaudio'; import { useStore } from '@nanostores/react'; import { FilesTab } from './FilesTab'; -import FileUpload from './FileUpload'; +import ImportSoundsButton from './ImportSoundsButton'; const TAURI = window.__TAURI__; @@ -243,7 +243,7 @@ function SoundsTab() { }); return (
-
+
settingsMap.setKey('soundsFilter', value)} @@ -254,6 +254,7 @@ function SoundsTab() { user: 'User', }} > + settingsMap.setKey('soundsFilter', 'user')} />
{soundEntries.map(([name, { data, onTrigger }]) => ( @@ -425,7 +426,6 @@ function SettingsTab({ scheduler }) { onChange={(fontFamily) => settingsMap.setKey('fontFamily', fontFamily)} /> - console.log(files)} /> a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' })) + .forEach((soundFile) => { + const name = soundFile.name; + if (!isAudioFile(name)) { + return; + } + const splitRelativePath = soundFile.webkitRelativePath?.split('/'); + const parentDirectory = splitRelativePath[splitRelativePath.length - 2]; + const soundPath = URL.createObjectURL(soundFile); + 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, + }); + }); + onComplete(); + } + + return ( + + ); +} From 8146725f89042944496c85643ec6eec49baa1cc1 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Mon, 4 Dec 2023 19:20:28 -0500 Subject: [PATCH 03/51] removed unessecary change --- packages/superdough/sampler.mjs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index d3e6a5a4..b8f10d5d 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -209,15 +209,14 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const { prebake, tag } = options; processSampleMap( sampleMap, - (key, value) => { - return registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { + (key, value) => + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { type: 'sample', samples: value, baseUrl, prebake, tag, - }); - }, + }), baseUrl, ); }; From a750490eeada9c269df3cc59739c6f5fe1ce01d9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 8 Dec 2023 08:49:03 +0100 Subject: [PATCH 04/51] begin pattern import button --- website/src/repl/panel/PatternsTab.jsx | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/website/src/repl/panel/PatternsTab.jsx b/website/src/repl/panel/PatternsTab.jsx index 1b19c8e0..fba6fbc9 100644 --- a/website/src/repl/panel/PatternsTab.jsx +++ b/website/src/repl/panel/PatternsTab.jsx @@ -44,6 +44,20 @@ export function PatternsTab({ context }) { +
{Object.entries(userPatterns).map(([key, up]) => ( ); } - -/* -selectable examples -if example selected - type character -> create new user pattern with exampleName_n - even if -clicking (+) opens the "new" example with same behavior as above -*/ From b03bf9ec3def256b2f643153fcafc56a3fa43919 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 8 Dec 2023 09:17:43 +0100 Subject: [PATCH 05/51] remove log --- website/src/settings.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 67d386cf..eaed555b 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -187,6 +187,5 @@ export function duplicateActivePattern() { } export function setActivePattern(key) { - console.log('set', key); settingsMap.setKey('activePattern', key); } From cec8553fb5e1eb03518bbddd8aea671d7cc39a41 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 8 Dec 2023 09:18:33 +0100 Subject: [PATCH 06/51] clear sounds + cps on pattern change + similar to shuffle --- website/src/repl/Repl.jsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 3d3117ae..7813ae98 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -247,9 +247,15 @@ export function Repl({ embedded = false }) { stop(); } }; - const handleUpdate = (newCode) => { + const handleUpdate = async (newCode, reset = false) => { + if (reset) { + clearCanvas(); + resetLoadedSounds(); + scheduler.setCps(1); + await prebake(); // declare default samples + } (newCode || isDirty) && activateCode(newCode); - logger('[repl] code updated! tip: you can also update the code by pressing ctrl+enter', 'highlight'); + logger('[repl] code updated!'); }; const handleShuffle = async () => { From 10d86ff83522d3eef3df06247f52a0685dfd1ed3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 8 Dec 2023 09:18:55 +0100 Subject: [PATCH 07/51] more style + set reset flag --- website/src/repl/panel/PatternsTab.jsx | 91 +++++++++++++++----------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/website/src/repl/panel/PatternsTab.jsx b/website/src/repl/panel/PatternsTab.jsx index fba6fbc9..5cbfc3b3 100644 --- a/website/src/repl/panel/PatternsTab.jsx +++ b/website/src/repl/panel/PatternsTab.jsx @@ -9,7 +9,9 @@ import { duplicateActivePattern, getUserPattern, renameActivePattern, + addUserPattern, } from '../../settings.mjs'; +import { logger } from '@strudel.cycles/core'; function classNames(...classes) { return classes.filter(Boolean).join(' '); @@ -18,16 +20,16 @@ function classNames(...classes) { export function PatternsTab({ context }) { const { userPatterns, activePattern } = useSettings(); return ( -
+

Pattern Collection

-
+
- {Object.entries(userPatterns).map(([key, up]) => ( -
{ - const { code } = up; - setActivePattern(key); - context.handleUpdate(code); - }} - > - {key} - - ))} +

Examples

- {Object.entries(tunes).map(([key, tune]) => ( - { - setActivePattern(key); - context.handleUpdate(tune); - }} - > - {key} - - ))} +
+ {Object.entries(tunes).map(([key, tune]) => ( + { + setActivePattern(key); + context.handleUpdate(tune, true); + }} + > + {key} + + ))} +
); From 8dd0e9600d51030dba058ae6947cb0cd67e65f7e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 8 Dec 2023 09:20:42 +0100 Subject: [PATCH 08/51] add some padding --- website/src/repl/panel/PatternsTab.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/panel/PatternsTab.jsx b/website/src/repl/panel/PatternsTab.jsx index 5cbfc3b3..880134ce 100644 --- a/website/src/repl/panel/PatternsTab.jsx +++ b/website/src/repl/panel/PatternsTab.jsx @@ -20,7 +20,7 @@ function classNames(...classes) { export function PatternsTab({ context }) { const { userPatterns, activePattern } = useSettings(); return ( -
+

Pattern Collection

From 31cd626820ea8b9b87ddba31082a490b06756f9d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 8 Dec 2023 09:30:06 +0100 Subject: [PATCH 09/51] style sounds and patterns tab more consistently --- website/src/repl/panel/PatternsTab.jsx | 3 +-- website/src/repl/panel/SoundsTab.jsx | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/website/src/repl/panel/PatternsTab.jsx b/website/src/repl/panel/PatternsTab.jsx index 880134ce..37cac6f7 100644 --- a/website/src/repl/panel/PatternsTab.jsx +++ b/website/src/repl/panel/PatternsTab.jsx @@ -22,8 +22,7 @@ export function PatternsTab({ context }) { return (
-

Pattern Collection

-
+
+ {!isExample && ( + + )} + {!isExample && ( + + )} +
+
+ )} + +
- - - @@ -66,24 +98,14 @@ export function PatternsTab({ context }) { /> import -
-
- {Object.entries(userPatterns).map(([key, up]) => ( - { - const { code } = up; - setActivePattern(key); - context.handleUpdate(code, true); - }} - > - {key} - - ))} +
From a6f969614e9e080e28f6f2a6a8ae77149763e151 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 8 Dec 2023 22:24:49 +0100 Subject: [PATCH 13/51] fix: rename cancel --- website/src/settings.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/src/settings.mjs b/website/src/settings.mjs index d660b396..620fb890 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -123,6 +123,10 @@ export function renameActivePattern() { return; } const newName = prompt('Enter new name', activePattern); + if (newName === null) { + // canceled + return; + } if (userPatterns[newName]) { alert('Name already taken!'); return; From 5116bf74ad267a244383ddf9474f7539820f8d96 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 8 Dec 2023 22:25:05 +0100 Subject: [PATCH 14/51] reorder pattern action buttons --- website/src/repl/panel/PatternsTab.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/repl/panel/PatternsTab.jsx b/website/src/repl/panel/PatternsTab.jsx index 124ba400..def6cef1 100644 --- a/website/src/repl/panel/PatternsTab.jsx +++ b/website/src/repl/panel/PatternsTab.jsx @@ -28,15 +28,15 @@ export function PatternsTab({ context }) {

{activePattern}

- {!isExample && ( )} + {!isExample && ( )} - {!isExample && ( - )} From 6969800192ae2e34974f44ff616375991e866aef Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 9 Dec 2023 17:23:24 +0100 Subject: [PATCH 17/51] fix: update button bug --- website/src/repl/Header.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/Header.jsx b/website/src/repl/Header.jsx index 5e991f8a..b6f13060 100644 --- a/website/src/repl/Header.jsx +++ b/website/src/repl/Header.jsx @@ -87,7 +87,7 @@ export function Header({ context }) { )}
diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 98d3fe50..2f76d68c 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -2,6 +2,7 @@ import { persistentMap } from '@nanostores/persistent'; import { useStore } from '@nanostores/react'; import { register } from '@strudel.cycles/core'; import * as tunes from './repl/tunes.mjs'; +import { logger } from '@strudel.cycles/core'; export const defaultSettings = { activeFooter: 'intro', @@ -195,3 +196,32 @@ export function setActivePattern(key) { } export function importUserPatternJSON(jsonString) {} + +export async function importPatterns(fileList) { + const files = Array.from(fileList); + await Promise.all( + files.map(async (file, i) => { + const content = await file.text(); + if (file.type === 'application/json') { + const userPatterns = getUserPatterns() || {}; + setUserPatterns({ ...userPatterns, ...JSON.parse(content) }); + } else if (file.type === 'text/plain') { + const name = file.name.replace(/\.[^/.]+$/, ''); + addUserPattern(name, { code: content }); + } + }), + ); + logger(`import done!`); +} + +export async function exportPatterns() { + const userPatterns = getUserPatterns() || {}; + const blob = new Blob([JSON.stringify(userPatterns)], { type: 'application/json' }); + const downloadLink = document.createElement('a'); + downloadLink.href = window.URL.createObjectURL(blob); + const date = new Date().toISOString().split('T')[0]; + downloadLink.download = `strudel_patterns_${date}.json`; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); +} From ace71693b725e5c7dd0edd5a1c6cb3a3fcce247c Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 10 Dec 2023 10:19:57 -0500 Subject: [PATCH 21/51] db utils should be reusable --- website/src/repl/ImportSoundsButton.jsx | 87 +++++++++++++------------ 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/website/src/repl/ImportSoundsButton.jsx b/website/src/repl/ImportSoundsButton.jsx index 502f25e4..0cc1f90f 100644 --- a/website/src/repl/ImportSoundsButton.jsx +++ b/website/src/repl/ImportSoundsButton.jsx @@ -3,8 +3,13 @@ import { registerSound, onTriggerSample } from '@strudel.cycles/webaudio'; import { isAudioFile } from './files.mjs'; //choose a directory to locally import samples -const userSamplesDB = 'testdb3'; -const sampleObject = 'testsamples'; + +const userSamplesDBConfig = { + dbName: 'samples', + table: 'usersamples', + columns: ['blob', 'title'], + version: 1, +}; function clearData() { window.indexedDB @@ -17,8 +22,8 @@ function clearData() { }); } -const registerSamples = () => { - openDB((objectStore) => { +const registerSamplesFromDB = (config) => { + openDB(config, (objectStore) => { let query = objectStore.getAll(); query.onsuccess = (event) => { const soundFiles = event.target.result; @@ -63,20 +68,20 @@ async function bufferToDataUrl(buf) { }); } -const openDB = (onOpened) => { - if ('indexedDB' in window) { - // indexedDB supported - } else { +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(userSamplesDB, 6); + const dbOpen = indexedDB.open(dbName, version); dbOpen.onupgradeneeded = (_event) => { const db = dbOpen.result; - const objectStore = db.createObjectStore(sampleObject, { keyPath: 'id', autoIncrement: false }); - // objectStore.createIndex('name', 'name', { unique: false }); - objectStore.createIndex('blob', 'blob', { unique: false }); - objectStore.createIndex('title', 'title', { unique: false }); + const objectStore = db.createObjectStore(table, { keyPath: 'id', autoIncrement: false }); + columns.forEach((c) => { + objectStore.createIndex(c, c, { unique: false }); + }); }; dbOpen.onerror = (err) => { console.error(`indexedDB error: ${err.errorCode}`); @@ -86,42 +91,43 @@ const openDB = (onOpened) => { const db = dbOpen.result; const // lock store for writing - writeTransaction = db.transaction([sampleObject], 'readwrite'), + writeTransaction = db.transaction([table], 'readwrite'), // get object store - objectStore = writeTransaction.objectStore(sampleObject); - // objectStore.put({ title: 'test', blob: 'test' }); - + 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; - } - // const id = crypto.randomUUID(); - const sUrl = URL.createObjectURL(s); - const buf = await fetch(sUrl).then((res) => res.arrayBuffer()); - const base64 = await bufferToDataUrl(buf); - const f = { - title, - blob: base64, - id: s.webkitRelativePath, - }; - return f; - }), + 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) => { console.error(error); }); }; -const setupUserSamplesDB = async (files, onComplete) => { - //clearData(); +const uploadSamplesToDB = async (config, files) => { + // clearData(); await processFilesForIDB(files).then((files) => { - const onOpened = (objectStore, db) => { + const onOpened = (objectStore, _db) => { files.forEach((file) => { if (file == null) { return; @@ -129,23 +135,22 @@ const setupUserSamplesDB = async (files, onComplete) => { const mutation = objectStore.put(file); mutation.onsuccess = () => {}; }); - onComplete(objectStore, db); }; - openDB(onOpened); + openDB(config, onOpened); }); }; export default function ImportSoundsButton({ onComplete }) { let fileUploadRef = React.createRef(); const onChange = useCallback(async () => { - await setupUserSamplesDB(fileUploadRef.current.files, (objectStore, db) => {}).then(async () => { - registerSamples(); + await uploadSamplesToDB(userSamplesDBConfig, fileUploadRef.current.files).then(() => { + registerSamplesFromDB(userSamplesDBConfig); onComplete(); }); }); useEffect(() => { - registerSamples(); + registerSamplesFromDB(userSamplesDBConfig); }, []); return ( From c4fe66421548bb56cfa3a4653400564b57815c75 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 10 Dec 2023 11:22:50 -0500 Subject: [PATCH 22/51] seperating things out --- website/src/repl/ImportSoundsButton.jsx | 160 ++---------------------- website/src/repl/Repl.jsx | 3 + website/src/repl/idbutils.mjs | 148 ++++++++++++++++++++++ 3 files changed, 164 insertions(+), 147 deletions(-) create mode 100644 website/src/repl/idbutils.mjs diff --git a/website/src/repl/ImportSoundsButton.jsx b/website/src/repl/ImportSoundsButton.jsx index 0cc1f90f..2ce208ae 100644 --- a/website/src/repl/ImportSoundsButton.jsx +++ b/website/src/repl/ImportSoundsButton.jsx @@ -1,164 +1,30 @@ -import React, { useCallback, useEffect } from 'react'; -import { registerSound, onTriggerSample } from '@strudel.cycles/webaudio'; -import { isAudioFile } from './files.mjs'; +import React, { useCallback, useState } from 'react'; +import { registerSamplesFromDB, uploadSamplesToDB, userSamplesDBConfig } from './idbutils.mjs'; //choose a directory to locally import samples - -const userSamplesDBConfig = { - dbName: 'samples', - table: 'usersamples', - columns: ['blob', 'title'], - version: 1, -}; - -function clearData() { - window.indexedDB - .databases() - .then((r) => { - for (var i = 0; i < r.length; i++) window.indexedDB.deleteDatabase(r[i].name); - }) - .then(() => { - alert('All data cleared.'); - }); -} - -const registerSamplesFromDB = (config) => { - openDB(config, (objectStore) => { - let query = objectStore.getAll(); - query.onsuccess = (event) => { - const soundFiles = event.target.result; - 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, - }); - }); - }; - }); -}; - -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); - }); -} - -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) => { - 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) => { - console.error(error); - }); -}; - -const uploadSamplesToDB = async (config, files) => { - // clearData(); - await processFilesForIDB(files).then((files) => { - const onOpened = (objectStore, _db) => { - files.forEach((file) => { - if (file == null) { - return; - } - const mutation = objectStore.put(file); - mutation.onsuccess = () => {}; - }); - }; - openDB(config, onOpened); - }); -}; - 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(); + registerSamplesFromDB(userSamplesDBConfig, () => { + onComplete(); + setIsUploading(false); + }); }); }); - useEffect(() => { - registerSamplesFromDB(userSamplesDBConfig); - }, []); - return ( ); } diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 2700bcf9..10f1cdda 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -24,6 +24,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(); @@ -183,6 +184,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); }); diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs new file mode 100644 index 00000000..414006b9 --- /dev/null +++ b/website/src/repl/idbutils.mjs @@ -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); + }); +}; From 4d361a2b3d345c0e953d21103cae25c4a6bc49bf Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 10 Dec 2023 11:24:39 -0500 Subject: [PATCH 23/51] fixing merge conflicts --- website/src/repl/Footer.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/website/src/repl/Footer.jsx b/website/src/repl/Footer.jsx index 7854d25e..5d1faf27 100644 --- a/website/src/repl/Footer.jsx +++ b/website/src/repl/Footer.jsx @@ -10,7 +10,6 @@ import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../s import { getAudioContext, soundMap } from '@strudel.cycles/webaudio'; import { useStore } from '@nanostores/react'; import { FilesTab } from './FilesTab'; -import ImportSoundsButton from './ImportSoundsButton'; const TAURI = window.__TAURI__; @@ -254,7 +253,6 @@ function SoundsTab() { user: 'User', }} > - settingsMap.setKey('soundsFilter', 'user')} />
{soundEntries.map(([name, { data, onTrigger }]) => ( From 145510266acef9736f421f26ba27345b3c141d00 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 10 Dec 2023 11:26:46 -0500 Subject: [PATCH 24/51] fixing merge conflicts --- website/src/repl/Footer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/Footer.jsx b/website/src/repl/Footer.jsx index 5d1faf27..65eeaf7f 100644 --- a/website/src/repl/Footer.jsx +++ b/website/src/repl/Footer.jsx @@ -242,7 +242,7 @@ function SoundsTab() { }); return (
-
+
settingsMap.setKey('soundsFilter', value)} From 4051276d74f3f2f8b200fa496d4bf0777c97ff4f Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 10 Dec 2023 11:27:39 -0500 Subject: [PATCH 25/51] fixing merge conflicts --- website/src/repl/Footer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/Footer.jsx b/website/src/repl/Footer.jsx index 65eeaf7f..b03f62d0 100644 --- a/website/src/repl/Footer.jsx +++ b/website/src/repl/Footer.jsx @@ -242,7 +242,7 @@ function SoundsTab() { }); return (
-
+
settingsMap.setKey('soundsFilter', value)} From 59a4cd9cf8a6dae5f044190759629fd67787bf48 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 10 Dec 2023 11:37:18 -0500 Subject: [PATCH 26/51] updating buton location --- website/src/repl/{ => panel}/ImportSoundsButton.jsx | 2 +- website/src/repl/panel/SoundsTab.jsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) rename website/src/repl/{ => panel}/ImportSoundsButton.jsx (97%) diff --git a/website/src/repl/ImportSoundsButton.jsx b/website/src/repl/panel/ImportSoundsButton.jsx similarity index 97% rename from website/src/repl/ImportSoundsButton.jsx rename to website/src/repl/panel/ImportSoundsButton.jsx index 2ce208ae..2d9c37a1 100644 --- a/website/src/repl/ImportSoundsButton.jsx +++ b/website/src/repl/panel/ImportSoundsButton.jsx @@ -1,5 +1,5 @@ import React, { useCallback, useState } from 'react'; -import { registerSamplesFromDB, uploadSamplesToDB, userSamplesDBConfig } from './idbutils.mjs'; +import { registerSamplesFromDB, uploadSamplesToDB, userSamplesDBConfig } from '../idbutils.mjs'; //choose a directory to locally import samples export default function ImportSoundsButton({ onComplete }) { diff --git a/website/src/repl/panel/SoundsTab.jsx b/website/src/repl/panel/SoundsTab.jsx index fe32212d..af9aabca 100644 --- a/website/src/repl/panel/SoundsTab.jsx +++ b/website/src/repl/panel/SoundsTab.jsx @@ -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 (
-
+
settingsMap.setKey('soundsFilter', value)} @@ -54,6 +55,7 @@ export function SoundsTab() { user: 'User', }} > + settingsMap.setKey('soundsFilter', 'user')} />
{soundEntries.map(([name, { data, onTrigger }]) => ( From 79e8e430f1fab853990cc52f53c604b0e388730c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 10 Dec 2023 19:50:20 +0100 Subject: [PATCH 27/51] fix: style for small screen --- website/src/repl/panel/Forms.jsx | 2 +- website/src/repl/panel/ImportSoundsButton.jsx | 2 +- website/src/repl/panel/SoundsTab.jsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/repl/panel/Forms.jsx b/website/src/repl/panel/Forms.jsx index e45305e5..49d0e913 100644 --- a/website/src/repl/panel/Forms.jsx +++ b/website/src/repl/panel/Forms.jsx @@ -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', diff --git a/website/src/repl/panel/ImportSoundsButton.jsx b/website/src/repl/panel/ImportSoundsButton.jsx index 2d9c37a1..20a1d5e3 100644 --- a/website/src/repl/panel/ImportSoundsButton.jsx +++ b/website/src/repl/panel/ImportSoundsButton.jsx @@ -21,7 +21,7 @@ export default function ImportSoundsButton({ onComplete }) { return (