basic samples tab

This commit is contained in:
Felix Roos 2022-11-12 20:17:01 +01:00
parent a27f399a9e
commit 5fc8f10602
4 changed files with 107 additions and 79 deletions

View File

@ -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 });

View File

@ -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];

View File

@ -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>

View File

@ -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/`),
]);
} }
} }