Merge pull request #164 from tidalcycles/talk-fixes

Talk fixes
This commit is contained in:
Felix Roos 2022-08-02 23:43:07 +02:00 committed by GitHub
commit 688a3d29fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 406 additions and 291 deletions

3
.gitignore vendored
View File

@ -34,4 +34,5 @@ repl_old
tutorial.rendered.mdx tutorial.rendered.mdx
doc.json doc.json
talk/public/EmuSP12 talk/public/EmuSP12
talk/public/samples talk/public/samples
server/samples/old

View File

@ -1046,6 +1046,22 @@ export class Pattern {
onTrigger(onTrigger) { onTrigger(onTrigger) {
return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger })); return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
} }
log(func = id) {
return this._withHap((hap) =>
hap.setContext({
...hap.context,
onTrigger: (...args) => {
if (hap.context.onTrigger) {
hap.context.onTrigger(...args);
}
console.log(func(...args));
},
}),
);
}
logValues(func = id) {
return this.log((_, hap) => func(hap.value));
}
} }
// TODO - adopt value.mjs fully.. // TODO - adopt value.mjs fully..

View File

@ -31,6 +31,19 @@ export const fromMidi = (n) => {
return Math.pow(2, (n - 69) / 12) * 440; return Math.pow(2, (n - 69) / 12) * 440;
}; };
export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') {
return fromMidi(noteOrMidi);
}
return fromMidi(toMidi(noteOrMidi));
};
export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1;
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
return pc + oct;
};
// modulo that works with negative numbers e.g. mod(-1, 3) = 2 // modulo that works with negative numbers e.g. mod(-1, 3) = 2
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m); // const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
export const mod = (n, m) => ((n % m) + m) % m; export const mod = (n, m) => ((n % m) + m) % m;

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import React, { useState, useEffect, useCallback, useMemo, useRef, useLayoutEffect } from 'react';
import { CodeMirror as CodeMirror$1 } from 'react-codemirror6'; import { CodeMirror as CodeMirror$1 } from 'react-codemirror6';
import { EditorView, Decoration } from '@codemirror/view'; import { EditorView, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state'; import { StateEffect, StateField } from '@codemirror/state';
@ -46,7 +46,13 @@ const materialPalenightTheme = EditorView.theme(
lineHeight: '22px', lineHeight: '22px',
}, },
'.cm-line': { '.cm-line': {
background: '#2C323699', // background: '#2C323699',
background: 'transparent',
},
'.cm-line > *': {
// background: '#2C323699',
background: '#00000090',
// background: 'transparent',
}, },
// done // done
'&.cm-focused .cm-cursor': { '&.cm-focused .cm-cursor': {
@ -71,7 +77,8 @@ const materialPalenightTheme = EditorView.theme(
backgroundColor: '#6199ff2f', backgroundColor: '#6199ff2f',
}, },
'.cm-activeLine': { backgroundColor: highlightBackground }, // commented out because it looks bad in mini repl one liners
//'.cm-activeLine': { backgroundColor: cursor + '50' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' }, '.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
@ -193,7 +200,7 @@ const highlightField = StateField.define({
if (from > l || to > l) { if (from > l || to > l) {
return; return;
} }
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } }); const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
return mark.range(from, to); return mark.range(from, to);
})).filter(Boolean), true); })).filter(Boolean), true);
} }
@ -351,6 +358,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
} }
}, [activeCode, onDrawProp]); }, [activeCode, onDrawProp]);
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
// cycle hook to control scheduling // cycle hook to control scheduling
const cycle = useCycle({ const cycle = useCycle({
onDraw, onDraw,
@ -449,6 +458,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}; };
return { return {
hideHeader,
hideConsole,
pending, pending,
code, code,
setCode, setCode,
@ -521,13 +532,13 @@ var tailwind = '';
var style = ''; var style = '';
const container = "_container_10e1g_1"; const container = "_container_xpa19_1";
const header = "_header_10e1g_5"; const header = "_header_xpa19_5";
const buttons = "_buttons_10e1g_9"; const buttons = "_buttons_xpa19_9";
const button = "_button_10e1g_9"; const button = "_button_xpa19_9";
const buttonDisabled = "_buttonDisabled_10e1g_17"; const buttonDisabled = "_buttonDisabled_xpa19_17";
const error = "_error_10e1g_21"; const error = "_error_xpa19_21";
const body = "_body_10e1g_25"; const body = "_body_xpa19_25";
var styles = { var styles = {
container: container, container: container,
header: header, header: header,
@ -563,12 +574,16 @@ function Icon({ type }) {
}[type]); }[type]);
} }
function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) { function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({ const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({
tune, tune,
defaultSynth, defaultSynth,
autolink: false autolink: false,
onEvent
}); });
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
const [view, setView] = useState(); const [view, setView] = useState();
const [ref, isVisible] = useInView({ const [ref, isVisible] = useInView({
threshold: 0.01 threshold: 0.01
@ -580,7 +595,25 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
} }
return isVisible || wasVisible.current; return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]); }, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started }); useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes("strudel disable-highlighting") });
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === "Enter") {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === "Period") {
cycle.stop();
e.preventDefault();
}
}
};
window.addEventListener("keydown", handleKeyPress, true);
return () => window.removeEventListener("keydown", handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
return /* @__PURE__ */ React.createElement("div", { return /* @__PURE__ */ React.createElement("div", {
className: styles.container, className: styles.container,
ref ref

View File

@ -1 +1 @@
*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cm-editor{background-color:transparent!important}._container_10e1g_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(68 76 87 / var(--tw-bg-opacity))}._header_10e1g_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_10e1g_9{display:flex}._button_10e1g_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_10e1g_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_10e1g_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_10e1g_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_10e1g_25{position:relative;overflow:auto} *,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cm-editor{background-color:transparent!important}._container_xpa19_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(17 17 17 / var(--tw-bg-opacity))}._header_xpa19_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_xpa19_9{display:flex}._button_xpa19_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_xpa19_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_xpa19_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_xpa19_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_xpa19_25{position:relative;overflow:auto}

View File

@ -60,7 +60,8 @@ const highlightField = StateField.define({
if (from > l || to > l) { if (from > l || to > l) {
return; // dont mark outside of range, as it will throw an error return; // dont mark outside of range, as it will throw an error
} }
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } }); // const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
return mark.range(from, to); return mark.range(from, to);
}), }),
) )

View File

@ -1,20 +1,25 @@
import React, { useState, useMemo, useRef } from 'react'; import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
import { useInView } from 'react-hook-inview'; import { useInView } from 'react-hook-inview';
import useRepl from '../hooks/useRepl.mjs'; import useRepl from '../hooks/useRepl.mjs';
import cx from '../cx'; import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs'; import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6 from './CodeMirror6'; import CodeMirror6, { flash } from './CodeMirror6';
import 'tailwindcss/tailwind.css'; import 'tailwindcss/tailwind.css';
import './style.css'; import './style.css';
import styles from './MiniRepl.module.css'; import styles from './MiniRepl.module.css';
import { Icon } from './Icon'; import { Icon } from './Icon';
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) { export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({ const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
tune, useRepl({
defaultSynth, tune,
autolink: false, defaultSynth,
}); autolink: false,
onEvent,
});
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
const [view, setView] = useState(); const [view, setView] = useState();
const [ref, isVisible] = useInView({ const [ref, isVisible] = useInView({
threshold: 0.01, threshold: 0.01,
@ -26,7 +31,28 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
} }
return isVisible || wasVisible.current; return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]); }, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started }); useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes('strudel disable-highlighting') });
// set active pattern on ctrl+enter
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
cycle.stop();
e.preventDefault();
}
}
};
window.addEventListener('keydown', handleKeyPress, true);
return () => window.removeEventListener('keydown', handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
return ( return (
<div className={styles.container} ref={ref}> <div className={styles.container} ref={ref}>
<div className={styles.header}> <div className={styles.header}>
@ -40,7 +66,7 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
</div> </div>
{error && <div className={styles.error}>{error.message}</div>} {error && <div className={styles.error}>{error.message}</div>}
</div> </div>
<div className={styles.body} > <div className={styles.body}>
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />} {show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
.container { .container {
@apply sc-rounded-md sc-overflow-hidden sc-bg-[#444C57]; @apply sc-rounded-md sc-overflow-hidden sc-bg-[#111111];
} }
.header { .header {

View File

@ -36,6 +36,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
} }
}, [activeCode, onDrawProp]); }, [activeCode, onDrawProp]);
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
// cycle hook to control scheduling // cycle hook to control scheduling
const cycle = useCycle({ const cycle = useCycle({
onDraw, onDraw,
@ -136,6 +138,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}; };
return { return {
hideHeader,
hideConsole,
pending, pending,
code, code,
setCode, setCode,

View File

@ -36,7 +36,13 @@ export const materialPalenightTheme = EditorView.theme(
lineHeight: '22px', lineHeight: '22px',
}, },
'.cm-line': { '.cm-line': {
background: '#2C323699', // background: '#2C323699',
background: 'transparent',
},
'.cm-line > *': {
// background: '#2C323699',
background: '#00000090',
// background: 'transparent',
}, },
// done // done
'&.cm-focused .cm-cursor': { '&.cm-focused .cm-cursor': {
@ -61,7 +67,8 @@ export const materialPalenightTheme = EditorView.theme(
backgroundColor: '#6199ff2f', backgroundColor: '#6199ff2f',
}, },
'.cm-activeLine': { backgroundColor: highlightBackground }, // commented out because it looks bad in mini repl one liners
//'.cm-activeLine': { backgroundColor: cursor + '50' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' }, '.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {

View File

@ -126,146 +126,150 @@ const splitSN = (s, n) => {
Pattern.prototype.out = function () { Pattern.prototype.out = function () {
return this.onTrigger(async (t, hap, ct) => { return this.onTrigger(async (t, hap, ct) => {
const ac = getAudioContext(); try {
// calculate correct time (tone.js workaround) const ac = getAudioContext();
t = ac.currentTime + t - ct; // calculate correct time (tone.js workaround)
// destructure value t = ac.currentTime + t - ct;
let { // destructure value
freq, let {
s, freq,
sf, s,
clip = 0, // if 1, samples will be cut off when the hap ends sf,
n = 0, clip = 0, // if 1, samples will be cut off when the hap ends
note, n = 0,
gain = 1, note,
cutoff, gain = 1,
resonance = 1, cutoff,
hcutoff, resonance = 1,
hresonance = 1, hcutoff,
bandf, hresonance = 1,
bandq = 1, bandf,
pan, bandq = 1,
attack = 0.001, pan,
decay = 0.05, attack = 0.001,
sustain = 0.5, decay = 0.05,
release = 0.001, sustain = 0.5,
speed = 1, // sample playback speed release = 0.001,
begin = 0, speed = 1, // sample playback speed
end = 1, begin = 0,
} = hap.value; end = 1,
const { velocity = 1 } = hap.context; } = hap.value;
gain *= velocity; // legacy fix for velocity const { velocity = 1 } = hap.context;
// the chain will hold all audio nodes that connect to each other gain *= velocity; // legacy fix for velocity
const chain = []; // the chain will hold all audio nodes that connect to each other
if (typeof s === 'string') { const chain = [];
[s, n] = splitSN(s, n); if (typeof s === 'string') {
} [s, n] = splitSN(s, n);
if (typeof note === 'string') {
[note, n] = splitSN(note, n);
}
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
// with synths, n and note are the same thing
n = note || n;
if (typeof n === 'string') {
n = toMidi(n); // e.g. c3 => 48
} }
// get frequency if (typeof note === 'string') {
if (!freq && typeof n === 'number') { [note, n] = splitSN(note, n);
freq = fromMidi(n); // + 48);
} }
// make oscillator if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
const o = getOscillator({ t, s, freq, duration: hap.duration, release }); // with synths, n and note are the same thing
chain.push(o); n = note || n;
// level down oscillators as they are really loud compared to samples i've tested if (typeof n === 'string') {
const g = ac.createGain(); n = toMidi(n); // e.g. c3 => 48
g.gain.value = 0.3;
chain.push(g);
// TODO: make adsr work with samples without pops
// envelope
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hap.duration);
chain.push(adsr);
} else {
// load sample
if (speed === 0) {
// no playback
return;
}
if (!s) {
console.warn('no sample specified');
return;
}
const soundfont = getSoundfontKey(s);
let bufferSource;
try {
if (soundfont) {
// is soundfont
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
} else {
// is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n, note);
} }
} catch (err) { // get frequency
console.warn(err); if (!freq && typeof n === 'number') {
return; freq = fromMidi(n); // + 48);
} }
// asny stuff above took too long? // make oscillator
if (ac.currentTime > t) { const o = getOscillator({ t, s, freq, duration: hap.duration, release });
console.warn('sample still loading:', s, n); chain.push(o);
return; // level down oscillators as they are really loud compared to samples i've tested
} const g = ac.createGain();
if (!bufferSource) { g.gain.value = 0.3;
console.warn('no buffer source'); chain.push(g);
return; // TODO: make adsr work with samples without pops
} // envelope
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value; const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hap.duration);
// TODO: nudge, unit, cut, loop chain.push(adsr);
let duration = soundfont || clip ? hap.duration : bufferSource.buffer.duration;
// let duration = bufferSource.buffer.duration;
const offset = begin * duration;
duration = ((end - begin) * duration) / Math.abs(speed);
if (soundfont || clip) {
bufferSource.start(t, offset); // duration does not work here for some reason
} else { } else {
bufferSource.start(t, offset, duration); // load sample
} if (speed === 0) {
chain.push(bufferSource); // no playback
if (soundfont || clip) { return;
const env = ac.createGain(); }
const releaseLength = 0.1; if (!s) {
env.gain.value = 0.6; console.warn('no sample specified');
env.gain.setValueAtTime(env.gain.value, t + duration); return;
env.gain.linearRampToValueAtTime(0, t + duration + releaseLength); }
// env.gain.linearRampToValueAtTime(0, t + duration + releaseLength); const soundfont = getSoundfontKey(s);
chain.push(env); let bufferSource;
bufferSource.stop(t + duration + releaseLength);
} else {
bufferSource.stop(t + duration);
}
}
// master out
const master = ac.createGain();
master.gain.value = gain;
chain.push(master);
// filters try {
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance)); if (soundfont) {
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance)); // is soundfont
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq)); bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
// TODO vowel } else {
// TODO delay / delaytime / delayfeedback // is sample from loaded samples(..)
// panning bufferSource = await getSampleBufferSource(s, n, note);
if (pan !== undefined) { }
const panner = ac.createStereoPanner(); } catch (err) {
panner.pan.value = 2 * pan - 1; console.warn(err);
chain.push(panner); return;
} }
// master out // asny stuff above took too long?
/* const master = ac.createGain(); if (ac.currentTime > t) {
console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) {
console.warn('no buffer source');
return;
}
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
// TODO: nudge, unit, cut, loop
let duration = soundfont || clip ? hap.duration : bufferSource.buffer.duration;
// let duration = bufferSource.buffer.duration;
const offset = begin * duration;
duration = ((end - begin) * duration) / Math.abs(speed);
if (soundfont || clip) {
bufferSource.start(t, offset); // duration does not work here for some reason
} else {
bufferSource.start(t, offset, duration);
}
chain.push(bufferSource);
if (soundfont || clip) {
const env = ac.createGain();
const releaseLength = 0.1;
env.gain.value = 0.6;
env.gain.setValueAtTime(env.gain.value, t + duration);
env.gain.linearRampToValueAtTime(0, t + duration + releaseLength);
// env.gain.linearRampToValueAtTime(0, t + duration + releaseLength);
chain.push(env);
bufferSource.stop(t + duration + releaseLength);
} else {
bufferSource.stop(t + duration);
}
}
// master out
const master = ac.createGain();
master.gain.value = gain;
chain.push(master);
// filters
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
// TODO vowel
// TODO delay / delaytime / delayfeedback
// panning
if (pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * pan - 1;
chain.push(panner);
}
// master out
/* const master = ac.createGain();
master.gain.value = 0.8 * gain; master.gain.value = 0.8 * gain;
chain.push(master); */ chain.push(master); */
chain.push(ac.destination); chain.push(ac.destination);
// connect chain elements together // connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
} catch (e) {
console.warn('.out error:', e);
}
}); });
}; };

3
repl/.gitignore vendored
View File

@ -23,4 +23,5 @@ dist-ssr
*.sln *.sln
*.sw? *.sw?
oldtunes.mjs oldtunes.mjs
public/samples/EMU World/

View File

@ -3,7 +3,7 @@
@tailwind utilities; @tailwind utilities;
body { body {
background-color: #2a3236; background-color: #111;
} }
.react-codemirror2, .react-codemirror2,
@ -15,12 +15,12 @@ body {
} }
.CodeMirror-line > span { .CodeMirror-line > span {
background-color: #2a323699; background-color: #11111190;
} }
.darken::before { .darken::before {
content: ' '; content: ' ';
position: absolute; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100vw; width: 100vw;

View File

@ -108,6 +108,8 @@ function App() {
pattern, pattern,
pushLog, pushLog,
pending, pending,
hideHeader,
hideConsole,
} = useRepl({ } = useRepl({
tune: '// LOADING...', tune: '// LOADING...',
defaultSynth, defaultSynth,
@ -167,142 +169,149 @@ function App() {
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
<header {!hideHeader && (
id="header" <header
className={cx( id="header"
'flex-none w-full px-2 flex border-b border-gray-200 justify-between z-[10] bg-gray-100', className={cx(
isEmbedded ? 'h-8' : 'h-14', 'flex-none w-full px-2 flex border-b border-gray-200 justify-between z-[10] bg-gray-100',
)} isEmbedded ? 'h-8' : 'h-14',
> )}
<div className="flex items-center space-x-2"> >
<img src={logo} className={cx('Tidal-logo', isEmbedded ? 'w-6 h-6' : 'w-10 h-10')} alt="logo" /> <div className="flex items-center space-x-2">
<h1 className={isEmbedded ? 'text-l' : 'text-xl'}>Strudel {isEmbedded ? 'Mini ' : ''}REPL</h1> <img src={logo} className={cx('Tidal-logo', isEmbedded ? 'w-6 h-6' : 'w-10 h-10')} alt="logo" />
</div> <h1 className={isEmbedded ? 'text-l' : 'text-xl'}>Strudel {isEmbedded ? 'Mini ' : ''}REPL</h1>
<div className="flex"> </div>
<button <div className="flex">
onClick={() => {
getAudioContext().resume(); // fixes no sound in ios webkit
togglePlay();
}}
className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
>
{!pending ? (
<span className={cx('flex items-center', isEmbedded ? 'w-16' : 'w-16')}>
{cycle.started ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
clipRule="evenodd"
/>
</svg>
)}
{cycle.started ? 'pause' : 'play'}
</span>
) : (
<>loading...</>
)}
</button>
{!isEmbedded && (
<button <button
className="hover:bg-gray-300 p-2" onClick={() => {
onClick={async () => { getAudioContext().resume(); // fixes no sound in ios webkit
const _code = getRandomTune(); togglePlay();
console.log('tune', _code); // uncomment this to debug when random code fails
setCode(_code);
cleanupDraw();
cleanupUi();
resetLoadedSamples();
prebake();
const parsed = await evaluate(_code);
setPattern(parsed.pattern);
setActiveCode(_code);
}} }}
className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
> >
🎲 random {!pending ? (
<span className={cx('flex items-center', isEmbedded ? 'w-16' : 'w-16')}>
{cycle.started ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
clipRule="evenodd"
/>
</svg>
)}
{cycle.started ? 'pause' : 'play'}
</span>
) : (
<>loading...</>
)}
</button> </button>
)} {!isEmbedded && (
{!isEmbedded && ( <button
<button className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}> className="hover:bg-gray-300 p-2"
<a href="./tutorial">📚 tutorial</a> onClick={async () => {
</button> const _code = getRandomTune();
)} console.log('tune', _code); // uncomment this to debug when random code fails
{!isEmbedded && ( setCode(_code);
<button cleanupDraw();
className={cx('cursor-pointer hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')} cleanupUi();
onClick={async () => { resetLoadedSamples();
const codeToShare = activeCode || code; prebake();
if (lastShared === codeToShare) { const parsed = await evaluate(_code);
// alert('Link already generated!'); setPattern(parsed.pattern);
pushLog(`Link already generated!`); setActiveCode(_code);
return;
}
// generate uuid in the browser
const hash = nanoid(12);
const { data, error } = await supabase.from('code').insert([{ code: codeToShare, hash }]);
if (!error) {
setLastShared(activeCode || code);
const shareUrl = window.location.origin + '?' + hash;
// copy shareUrl to clipboard
navigator.clipboard.writeText(shareUrl);
const message = `Link copied to clipboard: ${shareUrl}`;
// alert(message);
pushLog(message);
} else {
console.log('error', error);
const message = `Error: ${error.message}`;
// alert(message);
pushLog(message);
}
}}
>
📣 share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}
</button>
)}
{isEmbedded && (
<button className={cx('hover:bg-gray-300 px-2')}>
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
🚀 open
</a>
</button>
)}
{isEmbedded && (
<button className={cx('hover:bg-gray-300 px-2')}>
<a
onClick={() => {
window.location.href = initialUrl;
window.location.reload();
}} }}
title="Reset"
> >
💔 reset 🎲 random
</a> </button>
</button> )}
)} {!isEmbedded && (
</div> <button className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}>
</header> <a href="./tutorial">📚 tutorial</a>
</button>
)}
{!isEmbedded && (
<button
className={cx('cursor-pointer hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
onClick={async () => {
const codeToShare = activeCode || code;
if (lastShared === codeToShare) {
// alert('Link already generated!');
pushLog(`Link already generated!`);
return;
}
// generate uuid in the browser
const hash = nanoid(12);
const { data, error } = await supabase.from('code').insert([{ code: codeToShare, hash }]);
if (!error) {
setLastShared(activeCode || code);
const shareUrl = window.location.origin + '?' + hash;
// copy shareUrl to clipboard
navigator.clipboard.writeText(shareUrl);
const message = `Link copied to clipboard: ${shareUrl}`;
// alert(message);
pushLog(message);
} else {
console.log('error', error);
const message = `Error: ${error.message}`;
// alert(message);
pushLog(message);
}
}}
>
📣 share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}
</button>
)}
{isEmbedded && (
<button className={cx('hover:bg-gray-300 px-2')}>
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
🚀 open
</a>
</button>
)}
{isEmbedded && (
<button className={cx('hover:bg-gray-300 px-2')}>
<a
onClick={() => {
window.location.href = initialUrl;
window.location.reload();
}}
title="Reset"
>
💔 reset
</a>
</button>
)}
</div>
</header>
)}
<section className="grow flex flex-col text-gray-100"> <section className="grow flex flex-col text-gray-100">
<div className="grow relative flex overflow-auto" id="code"> <div className="grow relative flex overflow-auto pb-8 cursor-text" id="code">
{/* onCursor={markParens} */} {/* onCursor={markParens} */}
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} /> <CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
<span className="z-[20] py-1 px-2 absolute top-0 right-0 text-xs whitespace-pre text-right pointer-events-none"> <span className="z-[20] bg-black rounded-t-md py-1 px-2 fixed bottom-0 right-1 text-xs whitespace-pre text-right pointer-events-none">
{!cycle.started ? `press ctrl+enter to play\n` : dirty ? `ctrl+enter to update\n` : 'no changes\n'} {!cycle.started ? `press ctrl+enter to play\n` : dirty ? `ctrl+enter to update\n` : 'no changes\n'}
</span> </span>
{error && ( {error && (
<div className={cx('absolute right-2 bottom-2 px-2', 'text-red-500')}> <div
className={cx(
'rounded-md fixed pointer-events-none left-2 bottom-1 text-xs bg-black px-2 z-[20]',
'text-red-500',
)}
>
{error?.message || 'unknown error'} {error?.message || 'unknown error'}
</div> </div>
)} )}
</div> </div>
{!isEmbedded && ( {!isEmbedded && !hideConsole && (
<textarea <textarea
className="z-[10] h-16 border-0 text-xs bg-[transparent] border-t border-slate-600 resize-none" className="z-[10] h-16 border-0 text-xs bg-[transparent] border-t border-slate-600 resize-none"
value={log} value={log}