diff --git a/packages/webaudio/sampler.mjs b/packages/webaudio/sampler.mjs index 70e4d004..59dffb7c 100644 --- a/packages/webaudio/sampler.mjs +++ b/packages/webaudio/sampler.mjs @@ -19,6 +19,57 @@ function humanFileSize(bytes, si) { 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) => { const label = s ? `sound "${s}:${n}"` : 'sample'; if (!loadCache[url]) { @@ -27,7 +78,7 @@ export const loadBuffer = (url, ac, s, n = 0) => { loadCache[url] = fetch(url) .then((res) => res.arrayBuffer()) .then(async (res) => { - const took = (Date.now() - timestamp); + const took = Date.now() - timestamp; const size = humanFileSize(res.byteLength); // const downSpeed = humanFileSize(res.byteLength / took); logger(`[sampler] load ${label}... done! loaded ${size} in ${took}ms`, 'loaded-sample', { url }); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index a3f6549c..de93cdbe 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -9,7 +9,7 @@ import * as strudel from '@strudel.cycles/core'; import { fromMidi, logger, toMidi } from '@strudel.cycles/core'; import './feedbackdelay.mjs'; import './reverb.mjs'; -import { loadBuffer, reverseBuffer } from './sampler.mjs'; +import { getSampleBufferSource } from './sampler.mjs'; const { Pattern } = strudel; import './vowel.mjs'; import workletsUrl from './worklets.mjs?url'; @@ -98,57 +98,6 @@ const getSoundfontKey = (s) => { 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) => { if (!s.includes(':')) { return [s, n]; diff --git a/repl/src/App.jsx b/repl/src/App.jsx index 5edb72ce..7e0c574a 100644 --- a/repl/src/App.jsx +++ b/repl/src/App.jsx @@ -13,7 +13,7 @@ import logo from './logo.svg'; import * as tunes from './tunes.mjs'; import { prebake } from './prebake.mjs'; 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 { createClient } from '@supabase/supabase-js'; import { nanoid } from 'nanoid'; @@ -35,10 +35,7 @@ const supabase = createClient( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM', ); -evalScope( - // Tone, - controls, // sadly, this cannot be exported from core direclty - { WebDirt }, +const modules = [ import('@strudel.cycles/core'), // import('@strudel.cycles/tone'), import('@strudel.cycles/tonal'), @@ -49,9 +46,22 @@ evalScope( import('@strudel.cycles/osc'), import('@strudel.cycles/serial'), 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 pending = false; @@ -125,7 +135,7 @@ function App() { ); const footerContent = useRef(); useLayoutEffect(() => { - if (footerContent.current) { + if (footerContent.current && activeFooter === 'console') { // scroll log box to bottom when log changes footerContent.current.scrollTop = footerContent.current?.scrollHeight; } @@ -237,18 +247,18 @@ function App() { started && logger('[edit] code changed. hit ctrl+enter to update'); }; - const FooterTab = ({ label, children, type }) => ( + const FooterTab = ({ children, name }) => ( <>
setActiveFooter(type)} + onClick={() => setActiveFooter(name)} className={cx( - 'h-8 px-2 text-white cursor-pointer hover:text-highlight flex items-center space-x-1', - activeFooter === type ? 'border-b' : '', + 'h-8 px-2 text-white cursor-pointer hover:text-highlight flex items-center space-x-1 border-b', + activeFooter === name ? 'border-white hover:border-highlight' : 'border-transparent', )} > - {label} + {name}
- {activeFooter === type && <>{children}} + {activeFooter === name && <>{children}} ); @@ -362,10 +372,10 @@ function App() { diff --git a/repl/src/prebake.mjs b/repl/src/prebake.mjs index 0e5bb0d9..97443dcf 100644 --- a/repl/src/prebake.mjs +++ b/repl/src/prebake.mjs @@ -5,13 +5,15 @@ export async function prebake({ isMock = false, baseDir = '.' } = {}) { if (!isMock) { // https://archive.org/details/SalamanderGrandPianoV3 // License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm - samples('piano.json', `${baseDir}/piano/`); - // https://github.com/sgossner/VCSL/ - // https://api.github.com/repositories/126427031/contents/ - // LICENSE: CC0 general-purpose - samples('vcsl.json', 'github:sgossner/VCSL/master/'); - samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/'); - samples('EmuSP12.json', `${baseDir}/EmuSP12/`); + return await Promise.all([ + samples('piano.json', `${baseDir}/piano/`), + // https://github.com/sgossner/VCSL/ + // https://api.github.com/repositories/126427031/contents/ + // LICENSE: CC0 general-purpose + samples('vcsl.json', 'github:sgossner/VCSL/master/'), + samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/'), + samples('EmuSP12.json', `${baseDir}/EmuSP12/`), + ]); } }