mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48:34 +00:00
basic samples tab
This commit is contained in:
parent
a27f399a9e
commit
5fc8f10602
@ -19,6 +19,57 @@ function humanFileSize(bytes, si) {
|
|||||||
return bytes.toFixed(1) + ' ' + units[u];
|
return bytes.toFixed(1) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getSampleBufferSource = async (s, n, note, speed) => {
|
||||||
|
let transpose = 0;
|
||||||
|
let midi = typeof note === 'string' ? toMidi(note) : note || 36;
|
||||||
|
transpose = midi - 36; // C3 is middle C
|
||||||
|
|
||||||
|
const ac = getAudioContext();
|
||||||
|
// is sample from loaded samples(..)
|
||||||
|
const samples = getLoadedSamples();
|
||||||
|
if (!samples) {
|
||||||
|
throw new Error('no samples loaded');
|
||||||
|
}
|
||||||
|
const bank = samples?.[s];
|
||||||
|
if (!bank) {
|
||||||
|
throw new Error(
|
||||||
|
`sample not found: "${s}"`,
|
||||||
|
// , try one of ${Object.keys(samples)
|
||||||
|
// .map((s) => `"${s}"`)
|
||||||
|
// .join(', ')}.
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof bank !== 'object') {
|
||||||
|
throw new Error('wrong format for sample bank:', s);
|
||||||
|
}
|
||||||
|
let sampleUrl;
|
||||||
|
if (Array.isArray(bank)) {
|
||||||
|
sampleUrl = bank[n % bank.length];
|
||||||
|
} else {
|
||||||
|
const midiDiff = (noteA) => toMidi(noteA) - midi;
|
||||||
|
// object format will expect keys as notes
|
||||||
|
const closest = Object.keys(bank)
|
||||||
|
.filter((k) => !k.startsWith('_'))
|
||||||
|
.reduce(
|
||||||
|
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
transpose = -midiDiff(closest); // semitones to repitch
|
||||||
|
sampleUrl = bank[closest][n % bank[closest].length];
|
||||||
|
}
|
||||||
|
let buffer = await loadBuffer(sampleUrl, ac, s, n);
|
||||||
|
if (speed < 0) {
|
||||||
|
// should this be cached?
|
||||||
|
buffer = reverseBuffer(buffer);
|
||||||
|
}
|
||||||
|
const bufferSource = ac.createBufferSource();
|
||||||
|
bufferSource.buffer = buffer;
|
||||||
|
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
|
||||||
|
// bufferSource.playbackRate.value = Math.pow(2, transpose / 12);
|
||||||
|
bufferSource.playbackRate.value = playbackRate;
|
||||||
|
return bufferSource;
|
||||||
|
};
|
||||||
|
|
||||||
export const loadBuffer = (url, ac, s, n = 0) => {
|
export const loadBuffer = (url, ac, s, n = 0) => {
|
||||||
const label = s ? `sound "${s}:${n}"` : 'sample';
|
const label = s ? `sound "${s}:${n}"` : 'sample';
|
||||||
if (!loadCache[url]) {
|
if (!loadCache[url]) {
|
||||||
@ -27,7 +78,7 @@ export const loadBuffer = (url, ac, s, n = 0) => {
|
|||||||
loadCache[url] = fetch(url)
|
loadCache[url] = fetch(url)
|
||||||
.then((res) => res.arrayBuffer())
|
.then((res) => res.arrayBuffer())
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const took = (Date.now() - timestamp);
|
const took = Date.now() - timestamp;
|
||||||
const size = humanFileSize(res.byteLength);
|
const size = humanFileSize(res.byteLength);
|
||||||
// const downSpeed = humanFileSize(res.byteLength / took);
|
// const downSpeed = humanFileSize(res.byteLength / took);
|
||||||
logger(`[sampler] load ${label}... done! loaded ${size} in ${took}ms`, 'loaded-sample', { url });
|
logger(`[sampler] load ${label}... done! loaded ${size} in ${took}ms`, 'loaded-sample', { url });
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import * as strudel from '@strudel.cycles/core';
|
|||||||
import { fromMidi, logger, toMidi } from '@strudel.cycles/core';
|
import { fromMidi, logger, toMidi } from '@strudel.cycles/core';
|
||||||
import './feedbackdelay.mjs';
|
import './feedbackdelay.mjs';
|
||||||
import './reverb.mjs';
|
import './reverb.mjs';
|
||||||
import { loadBuffer, reverseBuffer } from './sampler.mjs';
|
import { getSampleBufferSource } from './sampler.mjs';
|
||||||
const { Pattern } = strudel;
|
const { Pattern } = strudel;
|
||||||
import './vowel.mjs';
|
import './vowel.mjs';
|
||||||
import workletsUrl from './worklets.mjs?url';
|
import workletsUrl from './worklets.mjs?url';
|
||||||
@ -98,57 +98,6 @@ const getSoundfontKey = (s) => {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSampleBufferSource = async (s, n, note, speed) => {
|
|
||||||
let transpose = 0;
|
|
||||||
let midi = typeof note === 'string' ? toMidi(note) : note || 36;
|
|
||||||
transpose = midi - 36; // C3 is middle C
|
|
||||||
|
|
||||||
const ac = getAudioContext();
|
|
||||||
// is sample from loaded samples(..)
|
|
||||||
const samples = getLoadedSamples();
|
|
||||||
if (!samples) {
|
|
||||||
throw new Error('no samples loaded');
|
|
||||||
}
|
|
||||||
const bank = samples?.[s];
|
|
||||||
if (!bank) {
|
|
||||||
throw new Error(
|
|
||||||
`sample not found: "${s}"`,
|
|
||||||
// , try one of ${Object.keys(samples)
|
|
||||||
// .map((s) => `"${s}"`)
|
|
||||||
// .join(', ')}.
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (typeof bank !== 'object') {
|
|
||||||
throw new Error('wrong format for sample bank:', s);
|
|
||||||
}
|
|
||||||
let sampleUrl;
|
|
||||||
if (Array.isArray(bank)) {
|
|
||||||
sampleUrl = bank[n % bank.length];
|
|
||||||
} else {
|
|
||||||
const midiDiff = (noteA) => toMidi(noteA) - midi;
|
|
||||||
// object format will expect keys as notes
|
|
||||||
const closest = Object.keys(bank)
|
|
||||||
.filter((k) => !k.startsWith('_'))
|
|
||||||
.reduce(
|
|
||||||
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
transpose = -midiDiff(closest); // semitones to repitch
|
|
||||||
sampleUrl = bank[closest][n % bank[closest].length];
|
|
||||||
}
|
|
||||||
let buffer = await loadBuffer(sampleUrl, ac, s, n);
|
|
||||||
if (speed < 0) {
|
|
||||||
// should this be cached?
|
|
||||||
buffer = reverseBuffer(buffer);
|
|
||||||
}
|
|
||||||
const bufferSource = ac.createBufferSource();
|
|
||||||
bufferSource.buffer = buffer;
|
|
||||||
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
|
|
||||||
// bufferSource.playbackRate.value = Math.pow(2, transpose / 12);
|
|
||||||
bufferSource.playbackRate.value = playbackRate;
|
|
||||||
return bufferSource;
|
|
||||||
};
|
|
||||||
|
|
||||||
const splitSN = (s, n) => {
|
const splitSN = (s, n) => {
|
||||||
if (!s.includes(':')) {
|
if (!s.includes(':')) {
|
||||||
return [s, n];
|
return [s, n];
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import logo from './logo.svg';
|
|||||||
import * as tunes from './tunes.mjs';
|
import * as tunes from './tunes.mjs';
|
||||||
import { prebake } from './prebake.mjs';
|
import { prebake } from './prebake.mjs';
|
||||||
import * as WebDirt from 'WebDirt';
|
import * as WebDirt from 'WebDirt';
|
||||||
import { resetLoadedSamples, getAudioContext } from '@strudel.cycles/webaudio';
|
import { resetLoadedSamples, getAudioContext, getLoadedSamples } from '@strudel.cycles/webaudio';
|
||||||
import { controls, evalScope, logger } from '@strudel.cycles/core';
|
import { controls, evalScope, logger } from '@strudel.cycles/core';
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
@ -35,10 +35,7 @@ const supabase = createClient(
|
|||||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
|
||||||
);
|
);
|
||||||
|
|
||||||
evalScope(
|
const modules = [
|
||||||
// Tone,
|
|
||||||
controls, // sadly, this cannot be exported from core direclty
|
|
||||||
{ WebDirt },
|
|
||||||
import('@strudel.cycles/core'),
|
import('@strudel.cycles/core'),
|
||||||
// import('@strudel.cycles/tone'),
|
// import('@strudel.cycles/tone'),
|
||||||
import('@strudel.cycles/tonal'),
|
import('@strudel.cycles/tonal'),
|
||||||
@ -49,9 +46,22 @@ evalScope(
|
|||||||
import('@strudel.cycles/osc'),
|
import('@strudel.cycles/osc'),
|
||||||
import('@strudel.cycles/serial'),
|
import('@strudel.cycles/serial'),
|
||||||
import('@strudel.cycles/soundfonts'),
|
import('@strudel.cycles/soundfonts'),
|
||||||
|
];
|
||||||
|
|
||||||
|
evalScope(
|
||||||
|
// Tone,
|
||||||
|
controls, // sadly, this cannot be exported from core direclty
|
||||||
|
{ WebDirt },
|
||||||
|
...modules,
|
||||||
);
|
);
|
||||||
|
|
||||||
prebake();
|
let loadedSamples = [];
|
||||||
|
const presets = prebake();
|
||||||
|
|
||||||
|
Promise.all([...modules, presets]).then((data) => {
|
||||||
|
// console.log('modules and sample registry loade', data);
|
||||||
|
loadedSamples = Object.entries(getLoadedSamples() || {});
|
||||||
|
});
|
||||||
|
|
||||||
const hideHeader = false;
|
const hideHeader = false;
|
||||||
const pending = false;
|
const pending = false;
|
||||||
@ -125,7 +135,7 @@ function App() {
|
|||||||
);
|
);
|
||||||
const footerContent = useRef();
|
const footerContent = useRef();
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (footerContent.current) {
|
if (footerContent.current && activeFooter === 'console') {
|
||||||
// scroll log box to bottom when log changes
|
// scroll log box to bottom when log changes
|
||||||
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
|
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
|
||||||
}
|
}
|
||||||
@ -237,18 +247,18 @@ function App() {
|
|||||||
started && logger('[edit] code changed. hit ctrl+enter to update');
|
started && logger('[edit] code changed. hit ctrl+enter to update');
|
||||||
};
|
};
|
||||||
|
|
||||||
const FooterTab = ({ label, children, type }) => (
|
const FooterTab = ({ children, name }) => (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
onClick={() => setActiveFooter(type)}
|
onClick={() => setActiveFooter(name)}
|
||||||
className={cx(
|
className={cx(
|
||||||
'h-8 px-2 text-white cursor-pointer hover:text-highlight flex items-center space-x-1',
|
'h-8 px-2 text-white cursor-pointer hover:text-highlight flex items-center space-x-1 border-b',
|
||||||
activeFooter === type ? 'border-b' : '',
|
activeFooter === name ? 'border-white hover:border-highlight' : 'border-transparent',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
{activeFooter === type && <>{children}</>}
|
{activeFooter === name && <>{children}</>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -362,10 +372,10 @@ function App() {
|
|||||||
</section>
|
</section>
|
||||||
<footer className="bg-footer">
|
<footer className="bg-footer">
|
||||||
<div className="flex justify-between px-2">
|
<div className="flex justify-between px-2">
|
||||||
<div className="flex">
|
<div className="flex pb-2">
|
||||||
<FooterTab type="help" label="Help" />
|
<FooterTab name="help" />
|
||||||
<FooterTab type="samples" label="Samples" />
|
<FooterTab name="samples" />
|
||||||
<FooterTab type="console" label="Console" />
|
<FooterTab name="console" />
|
||||||
</div>
|
</div>
|
||||||
{activeFooter !== '' && (
|
{activeFooter !== '' && (
|
||||||
<button onClick={() => setActiveFooter('')} className="text-white">
|
<button onClick={() => setActiveFooter('')} className="text-white">
|
||||||
@ -375,7 +385,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
{activeFooter !== '' && (
|
{activeFooter !== '' && (
|
||||||
<div
|
<div
|
||||||
className="text-white font-mono text-sm h-64 flex-none overflow-auto max-w-full break-all p-4"
|
className="text-white font-mono text-sm h-64 flex-none overflow-auto max-w-full break-all px-4"
|
||||||
ref={footerContent}
|
ref={footerContent}
|
||||||
>
|
>
|
||||||
{activeFooter === 'console' && (
|
{activeFooter === 'console' && (
|
||||||
@ -394,7 +404,23 @@ function App() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{activeFooter === 'samples' && <div>samples...</div>}
|
{activeFooter === 'samples' && (
|
||||||
|
<div className="break-normal w-full">
|
||||||
|
<span className="text-white">{loadedSamples.length} banks loaded:</span>
|
||||||
|
{loadedSamples.map(([name, samples]) => (
|
||||||
|
<span key={name} className="cursor-pointer hover:text-highlight" onClick={() => {}}>
|
||||||
|
{' '}
|
||||||
|
{name}(
|
||||||
|
{Array.isArray(samples)
|
||||||
|
? samples.length
|
||||||
|
: typeof samples === 'object'
|
||||||
|
? Object.values(samples).length
|
||||||
|
: 1}
|
||||||
|
){' '}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -5,13 +5,15 @@ export async function prebake({ isMock = false, baseDir = '.' } = {}) {
|
|||||||
if (!isMock) {
|
if (!isMock) {
|
||||||
// https://archive.org/details/SalamanderGrandPianoV3
|
// https://archive.org/details/SalamanderGrandPianoV3
|
||||||
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
|
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
|
||||||
samples('piano.json', `${baseDir}/piano/`);
|
return await Promise.all([
|
||||||
// https://github.com/sgossner/VCSL/
|
samples('piano.json', `${baseDir}/piano/`),
|
||||||
// https://api.github.com/repositories/126427031/contents/
|
// https://github.com/sgossner/VCSL/
|
||||||
// LICENSE: CC0 general-purpose
|
// https://api.github.com/repositories/126427031/contents/
|
||||||
samples('vcsl.json', 'github:sgossner/VCSL/master/');
|
// LICENSE: CC0 general-purpose
|
||||||
samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/');
|
samples('vcsl.json', 'github:sgossner/VCSL/master/'),
|
||||||
samples('EmuSP12.json', `${baseDir}/EmuSP12/`);
|
samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/'),
|
||||||
|
samples('EmuSP12.json', `${baseDir}/EmuSP12/`),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user