Merge pull request #106 from Bubobubobubobubo/import-samples
Import samples
This commit is contained in:
@ -377,6 +377,14 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span class="text-selection_foreground">Destroy universes</span>
|
<span class="text-selection_foreground">Destroy universes</span>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Upload audio samples -->
|
||||||
|
<p class="font-bold lg:text-xl text-sm ml-4 pb-2 pt-2 underline underline-offset-4 text-selection_background">Audio samples</p>
|
||||||
|
|
||||||
|
<label class="bg-brightwhite font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4 text-selection_background">
|
||||||
|
<svg class="rotate-180 fill-current w-4 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/></svg>
|
||||||
|
<input id="upload-samples" type="file" class="hidden" accept="file" webkitdirectory directory multiple>
|
||||||
|
<span id="sample-indicator" class="text-selection_foreground">Import samples</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,6 +18,8 @@ export const singleElements = {
|
|||||||
load_universe_button: "load-universe-button",
|
load_universe_button: "load-universe-button",
|
||||||
download_universe_button: "download-universes",
|
download_universe_button: "download-universes",
|
||||||
upload_universe_button: "upload-universes",
|
upload_universe_button: "upload-universes",
|
||||||
|
upload_samples_button: "upload-samples",
|
||||||
|
sample_indicator: "sample-indicator",
|
||||||
destroy_universes_button: "destroy-universes",
|
destroy_universes_button: "destroy-universes",
|
||||||
documentation_button: "doc-button-1",
|
documentation_button: "doc-button-1",
|
||||||
eval_button: "eval-button-1",
|
eval_button: "eval-button-1",
|
||||||
|
|||||||
155
src/IO/SampleLoading.ts
Normal file
155
src/IO/SampleLoading.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* This code is taken from https://github.com/tidalcycles/strudel/pull/839. The logic is written by
|
||||||
|
* daslyfe (Jade Rose Rowland). I have tweaked it a bit to fit the needs of this project (TypeScript),
|
||||||
|
* etc... Many thanks for this piece of code! This code is initially part of the Strudel project:
|
||||||
|
* https://github.com/tidalcycles/strudel.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { registerSound, onTriggerSample } from "superdough";
|
||||||
|
|
||||||
|
export const isAudioFile = (filename: string) => ['wav', 'mp3'].includes(filename.split('.').slice(-1)[0]);
|
||||||
|
|
||||||
|
interface samplesDBConfig {
|
||||||
|
dbName: string,
|
||||||
|
table: string,
|
||||||
|
columns: string[],
|
||||||
|
version: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const samplesDBConfig = {
|
||||||
|
dbName: 'samples',
|
||||||
|
table: 'usersamples',
|
||||||
|
columns: ['data_url', 'title'],
|
||||||
|
version: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bufferToDataUrl(buf: Buffer) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
var blob = new Blob([buf], { type: 'application/octet-binary' });
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function (event: Event) {
|
||||||
|
// @ts-ignore
|
||||||
|
resolve(event.target.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const processFilesForIDB = async (files: FileList) => {
|
||||||
|
return await Promise.all(
|
||||||
|
Array.from(files)
|
||||||
|
.map(async (s: File) => {
|
||||||
|
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
|
||||||
|
// @ts-ignore
|
||||||
|
// TODO: conversion to do here, remove ts-ignore
|
||||||
|
const base64 = await bufferToDataUrl(buf);
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
blob: base64,
|
||||||
|
id: s.webkitRelativePath,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean),
|
||||||
|
).catch((error) => {
|
||||||
|
console.log('Something went wrong while processing uploaded files', error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const registerSamplesFromDB = (config: samplesDBConfig, onComplete = () => {}) => {
|
||||||
|
openDB(config, (objectStore: IDBObjectStore) => {
|
||||||
|
let query = objectStore.getAll();
|
||||||
|
query.onsuccess = (event: Event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
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);
|
||||||
|
// @ts-ignore
|
||||||
|
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
|
||||||
|
type: 'sample',
|
||||||
|
samples: value,
|
||||||
|
baseUrl: undefined,
|
||||||
|
prebake: false,
|
||||||
|
tag: "user",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
onComplete();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openDB = (config: samplesDBConfig, onOpened: Function) => {
|
||||||
|
const { dbName, version, table, columns } = config
|
||||||
|
|
||||||
|
if (!('indexedDB' in window)) {
|
||||||
|
console.log('This browser doesn\'t support IndexedDB')
|
||||||
|
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: any) => {
|
||||||
|
objectStore.createIndex(c, c, { unique: false });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
dbOpen.onerror = function (err: Event) {
|
||||||
|
console.log('Error opening DB: ', (err.target as IDBOpenDBRequest).error);
|
||||||
|
}
|
||||||
|
dbOpen.onsuccess = function (_event: Event) {
|
||||||
|
const db = dbOpen.result;
|
||||||
|
db.onversionchange = function() {
|
||||||
|
db.close();
|
||||||
|
alert("Database is outdated, please reload the page.")
|
||||||
|
};
|
||||||
|
const writeTransaction = db.transaction([table], 'readwrite'),
|
||||||
|
objectStore = writeTransaction.objectStore(table);
|
||||||
|
// Writing in the database here!
|
||||||
|
onOpened(objectStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadSamplesToDB = async (config: samplesDBConfig, files: FileList) => {
|
||||||
|
await processFilesForIDB(files).then((files) => {
|
||||||
|
const onOpened = (objectStore: IDBObjectStore, _db: IDBDatabase) => {
|
||||||
|
// @ts-ignore
|
||||||
|
files.forEach((file: File) => {
|
||||||
|
if (file == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
objectStore.put(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
openDB(config, onOpened);
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -25,6 +25,7 @@ import { lineNumbers } from "@codemirror/view";
|
|||||||
import { jsCompletions } from "./EditorSetup";
|
import { jsCompletions } from "./EditorSetup";
|
||||||
import { createDocumentationStyle } from "./DomElements";
|
import { createDocumentationStyle } from "./DomElements";
|
||||||
import { saveState } from "./WindowBehavior";
|
import { saveState } from "./WindowBehavior";
|
||||||
|
import { registerSamplesFromDB, samplesDBConfig, uploadSamplesToDB } from "./IO/SampleLoading";
|
||||||
|
|
||||||
export const installInterfaceLogic = (app: Editor) => {
|
export const installInterfaceLogic = (app: Editor) => {
|
||||||
// Initialize style
|
// Initialize style
|
||||||
@ -159,6 +160,21 @@ export const installInterfaceLogic = (app: Editor) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.interface.upload_samples_button.addEventListener("input", async (event) => {
|
||||||
|
let fileInput = event.target as HTMLInputElement;
|
||||||
|
if (!fileInput.files?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
app.interface.sample_indicator.innerText = "Loading...";
|
||||||
|
app.interface.sample_indicator.classList.add("animate-pulse");
|
||||||
|
await uploadSamplesToDB(samplesDBConfig, fileInput.files).then(() => {
|
||||||
|
registerSamplesFromDB(samplesDBConfig, () => {
|
||||||
|
app.interface.sample_indicator.innerText = "Import samples";
|
||||||
|
app.interface.sample_indicator.classList.remove("animate-pulse");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.interface.upload_universe_button.addEventListener("click", () => {
|
app.interface.upload_universe_button.addEventListener("click", () => {
|
||||||
const fileInput = document.createElement("input");
|
const fileInput = document.createElement("input");
|
||||||
fileInput.type = "file";
|
fileInput.type = "file";
|
||||||
@ -582,4 +598,4 @@ export const installInterfaceLogic = (app: Editor) => {
|
|||||||
console.log("Could not find element " + name);
|
console.log("Could not find element " + name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -148,5 +148,13 @@ This sample pack is only one folder full of french phonems! It sounds super nice
|
|||||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||||
${samples_to_markdown(application, "Juliette")}
|
${samples_to_markdown(application, "Juliette")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## Your samples
|
||||||
|
|
||||||
|
These samples are the one you have loaded for the duration of the session using the <ic>Import Samples</ic> button in the configuration menu.
|
||||||
|
|
||||||
|
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||||
|
${samples_to_markdown(application, "user")}
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user