import useEvent from '@src/useEvent.mjs'; import { useStore } from '@nanostores/react'; import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio'; import { useMemo, useRef, useState } from 'react'; import { settingsMap, soundFilterType, useSettings } from '../../../settings.mjs'; import { ButtonGroup } from './Forms.jsx'; import ImportSoundsButton from './ImportSoundsButton.jsx'; import { Textbox } from '../textbox/Textbox.jsx'; import { ActionButton } from '../button/action-button.jsx'; import { confirmDialog } from '@src/repl/util.mjs'; import { clearIDB, userSamplesDBConfig } from '@src/repl/idbutils.mjs'; import { prebake } from '@src/repl/prebake.mjs'; const getSamples = (samples) => Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1; export function SoundsTab() { const sounds = useStore(soundMap); const { soundsFilter } = useSettings(); const [search, setSearch] = useState(''); const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; const soundEntries = useMemo(() => { if (!sounds) { return []; } let filtered = Object.entries(sounds) .filter(([key]) => !key.startsWith('_')) .sort((a, b) => a[0].localeCompare(b[0])) .filter(([name]) => name.toLowerCase().includes(search.toLowerCase())); if (soundsFilter === soundFilterType.USER) { return filtered.filter(([_, { data }]) => !data.prebake); } if (soundsFilter === soundFilterType.DRUMS) { return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines'); } if (soundsFilter === soundFilterType.SAMPLES) { return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines'); } if (soundsFilter === soundFilterType.SYNTHS) { return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); } if (soundsFilter === soundFilterType.WAVETABLES) { return filtered.filter(([_, { data }]) => data.type === 'wavetable'); } //TODO: tidy this up, it does not need to be saved in settings if (soundsFilter === 'importSounds') { return []; } return filtered; }, [sounds, soundsFilter, search]); // holds mutable ref to current triggered sound const trigRef = useRef(); // Used to cycle through sound previews on banks with multiple sounds let soundPreviewIdx = 0; // stop current sound on mouseup useEvent('mouseup', () => { const ref = trigRef.current; trigRef.current = undefined; ref?.stop?.(getAudioContext().currentTime + 0.01); }); return (
setSearch(v)} />
settingsMap.setKey('soundsFilter', value)} items={{ samples: 'samples', drums: 'drum-machines', synths: 'Synths', wavetables: 'Wavetables', user: 'User', importSounds: 'import-sounds', }} >
{soundsFilter === soundFilterType.USER && soundEntries.length > 0 && ( { try { const confirmed = await confirmDialog('Delete all imported user samples?'); if (confirmed) { clearIDB(userSamplesDBConfig.dbName); soundMap.set({}); await prebake(); } } catch (e) { console.error(e); } }} /> )}
{soundEntries.map(([name, { data, onTrigger }]) => { return ( { const ctx = getAudioContext(); const params = { note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined, s: name, n: soundPreviewIdx, clip: 1, release: 0.5, sustain: 1, duration: 0.5, }; soundPreviewIdx++; const onended = () => trigRef.current?.node?.disconnect(); try { // Pre-load the sample by calling onTrigger with a future time // This triggers the loading but schedules playback for later const time = ctx.currentTime + 0.05; const ref = await onTrigger(time, params, onended); trigRef.current = ref; if (ref?.node) { connectToDestination(ref.node); } } catch (err) { console.warn('Failed to trigger sound:', err); } }} > {' '} {name} {data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''} {data?.type === 'wavetable' ? `(${getSamples(data.tables)})` : ''} {data?.type === 'soundfont' ? `(${data.fonts.length})` : ''} ); })} {!soundEntries.length && soundsFilter === 'importSounds' ? (
settingsMap.setKey('soundsFilter', 'user')} />

To import sounds into strudel, they must be contained{' '} within a folder or subfolder . The best way to do this is to upload a “samples” folder containing subfolders of individual sounds or soundbanks (see diagram below).{' '}

              {`└─ samples <-- import this folder
   ├─ swoop
   │  ├─ swoopshort.wav
   │  ├─ swooplong.wav
   │  └─ swooptight.wav
   └─ smash
      ├─ smashhigh.wav
      ├─ smashlow.wav
      └─ smashmiddle.wav`}
            

The name of a subfolder corresponds to the sound name under the “user” tab. Multiple samples within a subfolder are all labelled with the same name, but can be accessed using “.n( )” - remember sounds are zero-indexed and in alphabetical order!

For more information, and other ways to use your own sounds in strudel,{' '} check out the docs !

Preview Sounds

              n("0 1 2 3 4 5").s("sample-name")
            

Paste the line above into the main editor to hear the uploaded folder. Remember to use the name of your sample as it appears under the "user" tab.

) : ( '' )} {!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''}
); }