Merge pull request #516 from tidalcycles/source-nodes
registerSound API + improved sounds tab + regroup soundfonts
This commit is contained in:
commit
18d62b32c2
26 changed files with 3267 additions and 949 deletions
|
|
@ -3,11 +3,12 @@ import { logger } from '@strudel.cycles/core';
|
|||
import { useEvent, cx } from '@strudel.cycles/react';
|
||||
// import { cx } from '@strudel.cycles/react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { loadedSamples } from './Repl';
|
||||
import React, { useMemo, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { Reference } from './Reference';
|
||||
import { themes } from './themes.mjs';
|
||||
import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../settings.mjs';
|
||||
import { getAudioContext, soundMap } from '@strudel.cycles/webaudio';
|
||||
import { useStore } from '@nanostores/react';
|
||||
|
||||
export function Footer({ context }) {
|
||||
const footerContent = useRef();
|
||||
|
|
@ -72,7 +73,7 @@ export function Footer({ context }) {
|
|||
<div className="flex justify-between px-2">
|
||||
<div className={cx('flex select-none max-w-full overflow-auto', activeFooter && 'pb-2')}>
|
||||
<FooterTab name="intro" label="welcome" />
|
||||
<FooterTab name="samples" />
|
||||
<FooterTab name="sounds" />
|
||||
<FooterTab name="console" />
|
||||
<FooterTab name="reference" />
|
||||
<FooterTab name="settings" />
|
||||
|
|
@ -84,13 +85,10 @@ export function Footer({ context }) {
|
|||
)}
|
||||
</div>
|
||||
{activeFooter !== '' && (
|
||||
<div
|
||||
className="text-white font-mono text-sm h-[360px] flex-none overflow-auto max-w-full relative"
|
||||
ref={footerContent}
|
||||
>
|
||||
<div className="text-white flex-none h-[360px] overflow-auto max-w-full relative" ref={footerContent}>
|
||||
{activeFooter === 'intro' && <WelcomeTab />}
|
||||
{activeFooter === 'console' && <ConsoleTab log={log} />}
|
||||
{activeFooter === 'samples' && <SamplesTab />}
|
||||
{activeFooter === 'sounds' && <SoundsTab />}
|
||||
{activeFooter === 'reference' && <Reference />}
|
||||
{activeFooter === 'settings' && <SettingsTab scheduler={context.scheduler} />}
|
||||
</div>
|
||||
|
|
@ -173,7 +171,7 @@ function WelcomeTab() {
|
|||
|
||||
function ConsoleTab({ log }) {
|
||||
return (
|
||||
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900">
|
||||
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm">
|
||||
<pre>{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
|
||||
██╔════╝╚══██╔══╝██╔══██╗██║ ██║██╔══██╗██╔════╝██║
|
||||
███████╗ ██║ ██████╔╝██║ ██║██║ ██║█████╗ ██║
|
||||
|
|
@ -193,36 +191,104 @@ function ConsoleTab({ log }) {
|
|||
);
|
||||
}
|
||||
|
||||
function SamplesTab() {
|
||||
const getSamples = (samples) =>
|
||||
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
||||
|
||||
function SoundsTab() {
|
||||
const sounds = useStore(soundMap);
|
||||
const { soundsFilter } = useSettings();
|
||||
const soundEntries = useMemo(() => {
|
||||
let filtered = Object.entries(sounds).filter(([key]) => !key.startsWith('_'));
|
||||
if (!sounds) {
|
||||
return [];
|
||||
}
|
||||
if (soundsFilter === 'user') {
|
||||
return filtered.filter(([key, { data }]) => !data.prebake);
|
||||
}
|
||||
if (soundsFilter === 'drums') {
|
||||
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
|
||||
}
|
||||
if (soundsFilter === 'samples') {
|
||||
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines');
|
||||
}
|
||||
if (soundsFilter === 'synths') {
|
||||
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
|
||||
}
|
||||
return filtered;
|
||||
}, [sounds, soundsFilter]);
|
||||
// holds mutable ref to current triggered sound
|
||||
const trigRef = useRef();
|
||||
// stop current sound on mouseup
|
||||
useEvent('mouseup', () => {
|
||||
const t = trigRef.current;
|
||||
trigRef.current = undefined;
|
||||
t?.then((ref) => {
|
||||
ref?.stop(getAudioContext().currentTime + 0.01);
|
||||
});
|
||||
});
|
||||
return (
|
||||
<div id="samples-tab" className="break-normal w-full px-4 dark:text-white text-stone-900">
|
||||
<span>{loadedSamples.length} banks loaded:</span>
|
||||
{loadedSamples.map(([name, samples]) => (
|
||||
<span key={name} className="cursor-pointer hover:opacity-50" onClick={() => {}}>
|
||||
{' '}
|
||||
{name}(
|
||||
{Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1}){' '}
|
||||
</span>
|
||||
))}
|
||||
<div id="sounds-tab" className="flex flex-col w-full h-full dark:text-white text-stone-900">
|
||||
<div className="px-2 pb-2 flex-none">
|
||||
<ButtonGroup
|
||||
value={soundsFilter}
|
||||
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
|
||||
items={{
|
||||
samples: 'samples',
|
||||
drums: 'drum-machines',
|
||||
synths: 'Synths',
|
||||
user: 'User',
|
||||
}}
|
||||
></ButtonGroup>
|
||||
</div>
|
||||
<div className="p-2 min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal">
|
||||
{soundEntries.map(([name, { data, onTrigger }]) => (
|
||||
<span
|
||||
key={name}
|
||||
className="cursor-pointer hover:opacity-50"
|
||||
onMouseDown={async () => {
|
||||
const ctx = getAudioContext();
|
||||
const params = {
|
||||
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
|
||||
s: name,
|
||||
clip: 1,
|
||||
release: 0.5,
|
||||
};
|
||||
const time = ctx.currentTime + 0.05;
|
||||
const onended = () => trigRef.current?.node?.disconnect();
|
||||
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
|
||||
trigRef.current.then((ref) => {
|
||||
ref?.node.connect(ctx.destination);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{name}
|
||||
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
|
||||
{data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
|
||||
</span>
|
||||
))}
|
||||
{!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroup({ value, onChange, items }) {
|
||||
return (
|
||||
<div className="flex grow border border-foreground rounded-md">
|
||||
<div className="flex max-w-lg">
|
||||
{Object.entries(items).map(([key, label], i, arr) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => onChange(key)}
|
||||
className={cx(
|
||||
'p-2 grow',
|
||||
i === 0 && 'rounded-l-md',
|
||||
i === arr.length - 1 && 'rounded-r-md',
|
||||
value === key ? 'bg-background' : 'bg-lineHighlight',
|
||||
'px-2 border-b h-8',
|
||||
// i === 0 && 'rounded-l-md',
|
||||
// i === arr.length - 1 && 'rounded-r-md',
|
||||
// value === key ? 'bg-background' : 'bg-lineHighlight',
|
||||
value === key ? 'border-foreground' : 'border-transparent',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{label.toLowerCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const visibleFunctions = jsdocJson.docs
|
|||
export function Reference() {
|
||||
return (
|
||||
<div className="flex h-full w-full pt-2 text-foreground">
|
||||
<div className="w-64 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
|
||||
<div className="w-42 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<a key={i} className="cursor-pointer block hover:bg-lineHighlight py-1 px-4" href={`#doc-${i}`}>
|
||||
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||
|
||||
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
|
||||
import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react';
|
||||
import {
|
||||
getAudioContext,
|
||||
getLoadedSamples,
|
||||
initAudioOnFirstClick,
|
||||
resetLoadedSamples,
|
||||
webaudioOutput,
|
||||
} from '@strudel.cycles/webaudio';
|
||||
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
|
|
@ -53,7 +47,6 @@ evalScope(
|
|||
...modules,
|
||||
);
|
||||
|
||||
export let loadedSamples = [];
|
||||
const presets = prebake();
|
||||
|
||||
let drawContext, clearCanvas;
|
||||
|
|
@ -62,11 +55,6 @@ if (typeof window !== 'undefined') {
|
|||
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
|
||||
}
|
||||
|
||||
Promise.all([...modules, presets]).then((data) => {
|
||||
// console.log('modules and sample registry loade', data);
|
||||
loadedSamples = Object.entries(getLoadedSamples() || {});
|
||||
});
|
||||
|
||||
const getTime = () => getAudioContext().currentTime;
|
||||
|
||||
async function initCode() {
|
||||
|
|
@ -211,7 +199,7 @@ export function Repl({ embedded = false }) {
|
|||
const { code, name } = getRandomTune();
|
||||
logger(`[repl] ✨ loading random tune "${name}"`);
|
||||
clearCanvas();
|
||||
resetLoadedSamples();
|
||||
resetLoadedSounds();
|
||||
await prebake(); // declare default samples
|
||||
await evaluate(code, false);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,17 +1,23 @@
|
|||
import { Pattern, toMidi, valueToMidi } from '@strudel.cycles/core';
|
||||
import { samples } from '@strudel.cycles/webaudio';
|
||||
//import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
||||
import { registerSynthSounds, samples } from '@strudel.cycles/webaudio';
|
||||
|
||||
export async function prebake() {
|
||||
// https://archive.org/details/SalamanderGrandPianoV3
|
||||
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
|
||||
return await Promise.all([
|
||||
samples(`./piano.json`, `./piano/`),
|
||||
registerSynthSounds();
|
||||
//registerSoundfonts();
|
||||
await Promise.all([
|
||||
samples(`./piano.json`, `./piano/`, { prebake: true }),
|
||||
// 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`, `./EmuSP12/`),
|
||||
samples(`./vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }),
|
||||
samples(`./tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', {
|
||||
prebake: true,
|
||||
tag: 'drum-machines',
|
||||
}),
|
||||
samples(`./EmuSP12.json`, `./EmuSP12/`, { prebake: true, tag: 'drum-machines' }),
|
||||
// samples('github:tidalcycles/Dirt-Samples/master'),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -566,7 +566,7 @@ samples({
|
|||
"C^7 Am7 Dm7 G7".slow(2).voicings('lefthand')
|
||||
.stack("0@6 [<1 2> <2 0> 1]@2".scale('C5 major'))
|
||||
.note().slow(4)
|
||||
.s('0040_FluidR3_GM_sf2_file')
|
||||
.s("gm_epiano1:1")
|
||||
.color('steelblue')
|
||||
.stack(
|
||||
"<-7 ~@2 [~@2 -7] -9 ~@2 [~@2 -9] -10!2 ~ [~@2 -10] -5 ~ [-3 -2 -10]@2>*2".scale('C3 major')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue