Compare commits

..

2 Commits

Author SHA1 Message Date
Felix Roos 684216bf8c strudel cli poc 2022-07-19 23:42:33 +02:00
Felix Roos ac564000b1 ignore samples 2022-07-19 23:29:11 +02:00
206 changed files with 1190 additions and 11554 deletions
+1 -2
View File
@@ -34,5 +34,4 @@ repl_old
tutorial.rendered.mdx
doc.json
talk/public/EmuSP12
talk/public/samples
server/samples/old
talk/public/samples
+9 -24
View File
@@ -1014,6 +1014,13 @@ export class Pattern {
return this._chunk(n, func, true);
}
edit(...funcs) {
return stack(...funcs.map((func) => func(this)));
}
pipe(func) {
return func(this);
}
_bypass(on) {
on = Boolean(parseInt(on));
return on ? silence : this;
@@ -1046,22 +1053,6 @@ export class Pattern {
onTrigger(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..
@@ -1222,9 +1213,8 @@ export function pure(value) {
export function isPattern(thing) {
// thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
const is = thing instanceof Pattern || thing?._Pattern;
if (thing?._Pattern && !thing instanceof Pattern) {
const is = thing instanceof Pattern || thing._Pattern;
if (!thing instanceof Pattern) {
console.warn(
`Found Pattern that fails "instanceof Pattern" check.
This may happen if you are using multiple versions of @strudel.cycles/core.
@@ -1567,8 +1557,3 @@ Pattern.prototype.define = (name, func, options = {}) => {
// Pattern.prototype.define('early', (a, pat) => pat.early(a), { patternified: true, composable: true });
Pattern.prototype.define('hush', (pat) => pat.hush(), { patternified: false, composable: true });
Pattern.prototype.define('bypass', (pat) => pat.bypass(on), { patternified: true, composable: true });
export async function loadScript(url) {
const code = await fetch(url).then((res) => res.text());
return eval(code);
}
-16
View File
@@ -19,9 +19,6 @@ export const tokenizeNote = (note) => {
// turns the given note into its midi number representation
export const toMidi = (note) => {
if (typeof note === 'number') {
return note;
}
const [pc, acc, oct] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
@@ -34,19 +31,6 @@ export const fromMidi = (n) => {
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
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
export const mod = (n, m) => ((n % m) + m) % m;
+2 -2
View File
File diff suppressed because one or more lines are too long
+18 -83
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo, useRef, useLayoutEffect } from 'react';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { CodeMirror as CodeMirror$1 } from 'react-codemirror6';
import { EditorView, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
@@ -46,13 +46,7 @@ const materialPalenightTheme = EditorView.theme(
lineHeight: '22px',
},
'.cm-line': {
// background: '#2C323699',
background: 'transparent',
},
'.cm-line > *': {
// background: '#2C323699',
background: '#00000090',
// background: 'transparent',
background: '#2C323699',
},
// done
'&.cm-focused .cm-cursor': {
@@ -77,7 +71,7 @@ const materialPalenightTheme = EditorView.theme(
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: cursor + '50' },
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
@@ -199,7 +193,7 @@ const highlightField = StateField.define({
if (from > l || to > l) {
return;
}
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
return mark.range(from, to);
})).filter(Boolean), true);
}
@@ -211,7 +205,7 @@ const highlightField = StateField.define({
},
provide: (f) => EditorView.decorations.from(f)
});
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount, theme }) {
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(CodeMirror$1, {
onViewChange: onViewChanged,
style: {
@@ -223,7 +217,7 @@ function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorD
onChange,
extensions: [
javascript(),
theme || materialPalenight,
materialPalenight,
highlightField,
flashField
]
@@ -294,13 +288,8 @@ function useCycle(props) {
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = (setZero = false) => {
if (setZero) {
Tone.getTransport().cancel();
Tone.getTransport().stop();
} else {
Tone.getTransport().pause();
}
const stop = () => {
Tone.getTransport().pause();
setStarted(false);
};
const toggle = () => (started ? stop() : start());
@@ -362,15 +351,13 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}
}, [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
const cycle = useCycle({
onDraw,
onEvent: useCallback(
(time, event, currentTime) => {
try {
onEvent?.(time, event, currentTime);
onEvent?.(event);
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
@@ -379,10 +366,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
} else if (!onEvent) {
throw new Error(
'no defaultSynth nor onEvent passed to useRepl + event has no onTrigger. nothing happens',
);
} else {
throw new Error('no defaultSynth passed to useRepl.');
}
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
@@ -423,16 +408,16 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
});
const activateCode = useCallback(
async (_code = code, evaluateOnly = false) => {
async (_code = code) => {
if (activeCode && !dirty) {
setError(undefined);
!evaluateOnly && cycle.start();
cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
!evaluateOnly && cycle.start();
cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
if (autolink) {
@@ -463,22 +448,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}
};
const stop = () => {
cycle.stop(true);
setActiveCode(undefined);
};
const evaluateOnly = () => {
activateCode(code, true);
};
useEffect(() => {
return () => stop();
}, []);
return {
hideHeader,
hideConsole,
pending,
code,
setCode,
@@ -489,10 +459,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
dirty,
log,
togglePlay,
stop,
setActiveCode,
activateCode,
evaluateOnly,
activeCode,
pushLog,
hash,
@@ -591,25 +559,16 @@ function Icon({ type }) {
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"
}),
stop: /* @__PURE__ */ React.createElement("path", {
fillRule: "evenodd",
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z",
clipRule: "evenodd"
})
}[type]);
}
function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({
function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
tune,
defaultSynth,
autolink: false,
onEvent
autolink: false
});
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01
@@ -621,25 +580,7 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, on
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
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]);
useHighlighting({ view, pattern, active: cycle.started });
return /* @__PURE__ */ React.createElement("div", {
className: styles.container,
ref
@@ -657,17 +598,11 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, on
onClick: () => activateCode()
}, /* @__PURE__ */ React.createElement(Icon, {
type: "refresh"
})), /* @__PURE__ */ React.createElement("button", {
className: cx(styles.button),
onClick: () => stop(true)
}, /* @__PURE__ */ React.createElement(Icon, {
type: "stop"
}))), error && /* @__PURE__ */ React.createElement("div", {
className: styles.error
}, error.message)), /* @__PURE__ */ React.createElement("div", {
className: styles.body
}, show && /* @__PURE__ */ React.createElement(CodeMirror, {
theme,
value: code,
onChange: setCode,
onViewChanged: setView
@@ -60,8 +60,7 @@ const highlightField = StateField.define({
if (from > l || to > l) {
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: 1.5px solid ${color};` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
return mark.range(from, to);
}),
)
@@ -79,7 +78,7 @@ const highlightField = StateField.define({
provide: (f) => EditorView.decorations.from(f),
});
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount, theme }) {
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
return (
<>
<_CodeMirror
@@ -93,7 +92,7 @@ export default function CodeMirror({ value, onChange, onViewChanged, onCursor, o
onChange={onChange}
extensions={[
javascript(),
theme || materialPalenight,
materialPalenight,
highlightField,
flashField,
// theme, language, ...
-7
View File
@@ -26,13 +26,6 @@ export function Icon({ type }) {
clipRule="evenodd"
/>
),
stop: (
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z"
clipRule="evenodd"
/>
),
}[type]
}
</svg>
+11 -40
View File
@@ -1,25 +1,20 @@
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
import React, { useState, useMemo, useRef } from 'react';
import { useInView } from 'react-hook-inview';
import useRepl from '../hooks/useRepl.mjs';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import CodeMirror6 from './CodeMirror6';
import 'tailwindcss/tailwind.css';
import './style.css';
import styles from './MiniRepl.module.css';
import { Icon } from './Icon';
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
useRepl({
tune,
defaultSynth,
autolink: false,
onEvent,
});
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
tune,
defaultSynth,
autolink: false,
});
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01,
@@ -31,28 +26,7 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, i
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
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]);
useHighlighting({ view, pattern, active: cycle.started });
return (
<div className={styles.container} ref={ref}>
<div className={styles.header}>
@@ -63,14 +37,11 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, i
<button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
<Icon type="refresh" />
</button>
<button className={cx(styles.button)} onClick={() => stop(true)}>
<Icon type="stop" />
</button>
</div>
{error && <div className={styles.error}>{error.message}</div>}
</div>
<div className={styles.body}>
{show && <CodeMirror6 theme={theme} value={code} onChange={setCode} onViewChanged={setView} />}
<div className={styles.body} >
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div>
</div>
);
+2 -7
View File
@@ -66,13 +66,8 @@ function useCycle(props) {
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = (setZero = false) => {
if (setZero) {
Tone.getTransport().cancel();
Tone.getTransport().stop();
} else {
Tone.getTransport().pause();
}
const stop = () => {
Tone.getTransport().pause();
setStarted(false);
};
const toggle = () => (started ? stop() : start());
+7 -28
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useCallback, useState, useMemo, useEffect } from 'react';
import { useCallback, useState, useMemo } from 'react';
import { evaluate } from '@strudel.cycles/eval';
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
import useCycle from './useCycle.mjs';
@@ -36,15 +36,13 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}
}, [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
const cycle = useCycle({
onDraw,
onEvent: useCallback(
(time, event, currentTime) => {
try {
onEvent?.(time, event, currentTime);
onEvent?.(event);
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
@@ -53,10 +51,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
} else if (!onEvent) {
throw new Error(
'no defaultSynth nor onEvent passed to useRepl + event has no onTrigger. nothing happens',
);
} else {
throw new Error('no defaultSynth passed to useRepl.');
}
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
@@ -97,16 +93,16 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
});
const activateCode = useCallback(
async (_code = code, evaluateOnly = false) => {
async (_code = code) => {
if (activeCode && !dirty) {
setError(undefined);
!evaluateOnly && cycle.start();
cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
!evaluateOnly && cycle.start();
cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
if (autolink) {
@@ -139,22 +135,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}
};
const stop = () => {
cycle.stop(true);
setActiveCode(undefined);
};
const evaluateOnly = () => {
activateCode(code, true);
};
useEffect(() => {
return () => stop();
}, []);
return {
hideHeader,
hideConsole,
pending,
code,
setCode,
@@ -165,10 +146,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
dirty,
log,
togglePlay,
stop,
setActiveCode,
activateCode,
evaluateOnly,
activeCode,
pushLog,
hash,
@@ -36,13 +36,7 @@ export const materialPalenightTheme = EditorView.theme(
lineHeight: '22px',
},
'.cm-line': {
// background: '#2C323699',
background: 'transparent',
},
'.cm-line > *': {
// background: '#2C323699',
background: '#00000090',
// background: 'transparent',
background: '#2C323699',
},
// done
'&.cm-focused .cm-cursor': {
@@ -67,7 +61,7 @@ export const materialPalenightTheme = EditorView.theme(
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: cursor + '50' },
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
+14 -24
View File
@@ -22,17 +22,13 @@ Pattern.prototype.pianoroll = function ({
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
inactive = '#C9E597',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 1,
autorange = 0,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
@@ -62,14 +58,12 @@ Pattern.prototype.pianoroll = function ({
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
const playheadPosition = scale(-from / timeExtent, ...timeRange);
this.draw(
(ctx, events, t) => {
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
events.filter(inFrame).forEach((event) => {
@@ -77,6 +71,15 @@ Pattern.prototype.pianoroll = function ({
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
@@ -104,19 +107,6 @@ Pattern.prototype.pianoroll = function ({
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
ctx.strokeStyle = playheadColor;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
},
{
from: from - overscan,
+1 -5
View File
@@ -62,11 +62,7 @@ Pattern.prototype.tone = function (instrument) {
} else if (instrument instanceof NoiseSynth) {
instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
} else if (instrument instanceof Piano) {
// note = getPlayableNoteValue(hap); // piano does not understand frequencies
note = hap.value;
if (typeof note === 'number') {
note = midi2note(note);
}
note = getPlayableNoteValue(hap);
instrument.keyDown({ note, time, velocity });
instrument.keyUp({ note, time: time + hap.duration.valueOf(), velocity });
} else if (instrument instanceof Sampler) {
+133 -137
View File
@@ -126,150 +126,146 @@ const splitSN = (s, n) => {
Pattern.prototype.out = function () {
return this.onTrigger(async (t, hap, ct) => {
try {
const ac = getAudioContext();
// calculate correct time (tone.js workaround)
t = ac.currentTime + t - ct;
// destructure value
let {
freq,
s,
sf,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
gain = 1,
cutoff,
resonance = 1,
hcutoff,
hresonance = 1,
bandf,
bandq = 1,
pan,
attack = 0.001,
decay = 0.05,
sustain = 0.5,
release = 0.001,
speed = 1, // sample playback speed
begin = 0,
end = 1,
} = hap.value;
const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity
// the chain will hold all audio nodes that connect to each other
const chain = [];
if (typeof s === 'string') {
[s, n] = splitSN(s, n);
const ac = getAudioContext();
// calculate correct time (tone.js workaround)
t = ac.currentTime + t - ct;
// destructure value
let {
freq,
s,
sf,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
gain = 1,
cutoff,
resonance = 1,
hcutoff,
hresonance = 1,
bandf,
bandq = 1,
pan,
attack = 0.001,
decay = 0.05,
sustain = 0.5,
release = 0.001,
speed = 1, // sample playback speed
begin = 0,
end = 1,
} = hap.value;
const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity
// the chain will hold all audio nodes that connect to each other
const chain = [];
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
}
if (typeof note === 'string') {
[note, n] = splitSN(note, n);
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
}
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
// make oscillator
const o = getOscillator({ t, s, freq, duration: hap.duration, release });
chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
const g = ac.createGain();
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);
}
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
}
// make oscillator
const o = getOscillator({ t, s, freq, duration: hap.duration, release });
chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
const g = ac.createGain();
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);
} catch (err) {
console.warn(err);
return;
}
// asny stuff above took too long?
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 {
// 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) {
console.warn(err);
return;
}
// asny stuff above took too long?
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);
}
bufferSource.start(t, offset, 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);
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 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;
chain.push(master); */
chain.push(ac.destination);
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
} catch (e) {
console.warn('.out error:', e);
}
chain.push(ac.destination);
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
});
};
+2 -2
View File
@@ -6,6 +6,6 @@ This package adds [webdirt](https://github.com/dktr0/WebDirt) support to strudel
Add default samples to repl:
1. move samples to `repl/public/samples` folder. the samples folder is expected to have subfolders, with samples in it. the subfolders will be the names of the samples.
2. run `./makeSampleMap.sh ../../repl/public/samples/EmuSP12 > ../../repl/public/samples/EmuSP12.json`
1. move samples to `repl/public` folder. the samples folder is expected to have subfolders, with samples in it. the subfolders will be the names of the samples.
2. run `./makeSampleMap.sh ../../repl/public/EmuSP12 > ../../repl/public/EmuSP12.json`
3. adapt `loadWebDirt` in App.jsx + MiniRepl.jsx to use the generated json file
-1
View File
@@ -24,5 +24,4 @@ dist-ssr
*.sw?
oldtunes.mjs
public/samples/EMU World

Some files were not shown because too many files have changed in this diff Show More