mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-21 20:55:12 -04:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 235f3672aa | |||
| 55dbcdde18 | |||
| d3f10518cc | |||
| 99fd5a53cd | |||
| 7e21b683fb | |||
| a492628b33 | |||
| 53bb20c516 | |||
| 588f74123e | |||
| e3473d5e70 | |||
| 8ce4de0c8d | |||
| b87cea834f | |||
| 230ebe7439 | |||
| e1f017916c | |||
| 6a445ee130 | |||
| 8ea0044ffa | |||
| 2d76de5906 | |||
| 4cce65c9fe | |||
| 6b3687357e | |||
| 1d6e74fc77 | |||
| c9fda97534 | |||
| 8796bcbdc3 | |||
| 4c66d9a553 | |||
| 85d3d7392e | |||
| 31d0225341 | |||
| d0acb32497 | |||
| aa9a32c9af | |||
| a8610d0078 | |||
| ef5dcd0dae | |||
| ed86a94f99 | |||
| 7be791f2d3 | |||
| cc31a21b6f | |||
| 72155d24c9 | |||
| 7672ca9971 | |||
| 07120e9e3b | |||
| 72feb3b65a | |||
| 2f5ff44dc9 | |||
| 8051286904 | |||
| 8a57333f72 | |||
| 512ba03fef | |||
| 4f0d2f181b | |||
| 1339bc3b94 | |||
| edf2fb9781 | |||
| cf6bfd0e30 | |||
| a93d6e884d | |||
| 1a9b57e66c | |||
| 0aec9423ac | |||
| 54df4edca7 | |||
| be6310b8e2 | |||
| 3ab32114a6 | |||
| da85e1920b | |||
| 352d701b7b | |||
| 349c1bc5d8 | |||
| 9430caeb29 | |||
| eb0adf4194 | |||
| 6fde3f2231 | |||
| 1c77e7e5b3 | |||
| 061abecaca | |||
| 98c131df9a | |||
| 1af10157d6 | |||
| a3eeaead2a | |||
| ff362da23f |
@@ -35,3 +35,4 @@ tutorial.rendered.mdx
|
|||||||
doc.json
|
doc.json
|
||||||
talk/public/EmuSP12
|
talk/public/EmuSP12
|
||||||
talk/public/samples
|
talk/public/samples
|
||||||
|
server/samples/old
|
||||||
@@ -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..
|
||||||
@@ -1206,8 +1222,9 @@ export function pure(value) {
|
|||||||
|
|
||||||
export function isPattern(thing) {
|
export function isPattern(thing) {
|
||||||
// thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
|
// thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
|
||||||
const is = thing instanceof Pattern || thing._Pattern;
|
const is = thing instanceof Pattern || thing?._Pattern;
|
||||||
if (!thing instanceof Pattern) {
|
|
||||||
|
if (thing?._Pattern && !thing instanceof Pattern) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Found Pattern that fails "instanceof Pattern" check.
|
`Found Pattern that fails "instanceof Pattern" check.
|
||||||
This may happen if you are using multiple versions of @strudel.cycles/core.
|
This may happen if you are using multiple versions of @strudel.cycles/core.
|
||||||
@@ -1550,3 +1567,8 @@ Pattern.prototype.define = (name, func, options = {}) => {
|
|||||||
// Pattern.prototype.define('early', (a, pat) => pat.early(a), { patternified: true, composable: true });
|
// 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('hush', (pat) => pat.hush(), { patternified: false, composable: true });
|
||||||
Pattern.prototype.define('bypass', (pat) => pat.bypass(on), { patternified: true, 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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export const tokenizeNote = (note) => {
|
|||||||
|
|
||||||
// turns the given note into its midi number representation
|
// turns the given note into its midi number representation
|
||||||
export const toMidi = (note) => {
|
export const toMidi = (note) => {
|
||||||
|
if (typeof note === 'number') {
|
||||||
|
return note;
|
||||||
|
}
|
||||||
const [pc, acc, oct] = tokenizeNote(note);
|
const [pc, acc, oct] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
throw new Error('not a note: "' + note + '"');
|
throw new Error('not a note: "' + note + '"');
|
||||||
@@ -31,6 +34,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;
|
||||||
|
|||||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+83
-18
@@ -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,7 @@ const materialPalenightTheme = EditorView.theme(
|
|||||||
backgroundColor: '#6199ff2f',
|
backgroundColor: '#6199ff2f',
|
||||||
},
|
},
|
||||||
|
|
||||||
'.cm-activeLine': { backgroundColor: highlightBackground },
|
'.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 +199,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);
|
||||||
}
|
}
|
||||||
@@ -205,7 +211,7 @@ const highlightField = StateField.define({
|
|||||||
},
|
},
|
||||||
provide: (f) => EditorView.decorations.from(f)
|
provide: (f) => EditorView.decorations.from(f)
|
||||||
});
|
});
|
||||||
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
|
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount, theme }) {
|
||||||
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(CodeMirror$1, {
|
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(CodeMirror$1, {
|
||||||
onViewChange: onViewChanged,
|
onViewChange: onViewChanged,
|
||||||
style: {
|
style: {
|
||||||
@@ -217,7 +223,7 @@ function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorD
|
|||||||
onChange,
|
onChange,
|
||||||
extensions: [
|
extensions: [
|
||||||
javascript(),
|
javascript(),
|
||||||
materialPalenight,
|
theme || materialPalenight,
|
||||||
highlightField,
|
highlightField,
|
||||||
flashField
|
flashField
|
||||||
]
|
]
|
||||||
@@ -288,8 +294,13 @@ function useCycle(props) {
|
|||||||
await Tone.start();
|
await Tone.start();
|
||||||
Tone.getTransport().start('+0.1');
|
Tone.getTransport().start('+0.1');
|
||||||
};
|
};
|
||||||
const stop = () => {
|
const stop = (setZero = false) => {
|
||||||
Tone.getTransport().pause();
|
if (setZero) {
|
||||||
|
Tone.getTransport().cancel();
|
||||||
|
Tone.getTransport().stop();
|
||||||
|
} else {
|
||||||
|
Tone.getTransport().pause();
|
||||||
|
}
|
||||||
setStarted(false);
|
setStarted(false);
|
||||||
};
|
};
|
||||||
const toggle = () => (started ? stop() : start());
|
const toggle = () => (started ? stop() : start());
|
||||||
@@ -351,13 +362,15 @@ 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,
|
||||||
onEvent: useCallback(
|
onEvent: useCallback(
|
||||||
(time, event, currentTime) => {
|
(time, event, currentTime) => {
|
||||||
try {
|
try {
|
||||||
onEvent?.(event);
|
onEvent?.(time, event, currentTime);
|
||||||
if (event.context.logs?.length) {
|
if (event.context.logs?.length) {
|
||||||
event.context.logs.forEach(pushLog);
|
event.context.logs.forEach(pushLog);
|
||||||
}
|
}
|
||||||
@@ -366,8 +379,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
|
|||||||
if (defaultSynth) {
|
if (defaultSynth) {
|
||||||
const note = getPlayableNoteValue(event);
|
const note = getPlayableNoteValue(event);
|
||||||
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
||||||
} else {
|
} else if (!onEvent) {
|
||||||
throw new Error('no defaultSynth passed to useRepl.');
|
throw new Error(
|
||||||
|
'no defaultSynth nor onEvent passed to useRepl + event has no onTrigger. nothing happens',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
/* console.warn('no instrument chosen', event);
|
/* console.warn('no instrument chosen', event);
|
||||||
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
||||||
@@ -408,16 +423,16 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
|
|||||||
});
|
});
|
||||||
|
|
||||||
const activateCode = useCallback(
|
const activateCode = useCallback(
|
||||||
async (_code = code) => {
|
async (_code = code, evaluateOnly = false) => {
|
||||||
if (activeCode && !dirty) {
|
if (activeCode && !dirty) {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
cycle.start();
|
!evaluateOnly && cycle.start();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setPending(true);
|
setPending(true);
|
||||||
const parsed = await evaluate(_code);
|
const parsed = await evaluate(_code);
|
||||||
cycle.start();
|
!evaluateOnly && cycle.start();
|
||||||
broadcast({ type: 'start', from: id });
|
broadcast({ type: 'start', from: id });
|
||||||
setPattern(() => parsed.pattern);
|
setPattern(() => parsed.pattern);
|
||||||
if (autolink) {
|
if (autolink) {
|
||||||
@@ -448,7 +463,22 @@ 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 {
|
return {
|
||||||
|
hideHeader,
|
||||||
|
hideConsole,
|
||||||
pending,
|
pending,
|
||||||
code,
|
code,
|
||||||
setCode,
|
setCode,
|
||||||
@@ -459,8 +489,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
|
|||||||
dirty,
|
dirty,
|
||||||
log,
|
log,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
|
stop,
|
||||||
setActiveCode,
|
setActiveCode,
|
||||||
activateCode,
|
activateCode,
|
||||||
|
evaluateOnly,
|
||||||
activeCode,
|
activeCode,
|
||||||
pushLog,
|
pushLog,
|
||||||
hash,
|
hash,
|
||||||
@@ -559,16 +591,25 @@ function Icon({ type }) {
|
|||||||
fillRule: "evenodd",
|
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",
|
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"
|
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]);
|
}[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 +621,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
|
||||||
@@ -598,11 +657,17 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
|
|||||||
onClick: () => activateCode()
|
onClick: () => activateCode()
|
||||||
}, /* @__PURE__ */ React.createElement(Icon, {
|
}, /* @__PURE__ */ React.createElement(Icon, {
|
||||||
type: "refresh"
|
type: "refresh"
|
||||||
|
})), /* @__PURE__ */ React.createElement("button", {
|
||||||
|
className: cx(styles.button),
|
||||||
|
onClick: () => stop(true)
|
||||||
|
}, /* @__PURE__ */ React.createElement(Icon, {
|
||||||
|
type: "stop"
|
||||||
}))), error && /* @__PURE__ */ React.createElement("div", {
|
}))), error && /* @__PURE__ */ React.createElement("div", {
|
||||||
className: styles.error
|
className: styles.error
|
||||||
}, error.message)), /* @__PURE__ */ React.createElement("div", {
|
}, error.message)), /* @__PURE__ */ React.createElement("div", {
|
||||||
className: styles.body
|
className: styles.body
|
||||||
}, show && /* @__PURE__ */ React.createElement(CodeMirror, {
|
}, show && /* @__PURE__ */ React.createElement(CodeMirror, {
|
||||||
|
theme,
|
||||||
value: code,
|
value: code,
|
||||||
onChange: setCode,
|
onChange: setCode,
|
||||||
onViewChanged: setView
|
onViewChanged: setView
|
||||||
|
|||||||
@@ -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);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -78,7 +79,7 @@ const highlightField = StateField.define({
|
|||||||
provide: (f) => EditorView.decorations.from(f),
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
|
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount, theme }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<_CodeMirror
|
<_CodeMirror
|
||||||
@@ -92,7 +93,7 @@ export default function CodeMirror({ value, onChange, onViewChanged, onCursor, o
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
extensions={[
|
extensions={[
|
||||||
javascript(),
|
javascript(),
|
||||||
materialPalenight,
|
theme || materialPalenight,
|
||||||
highlightField,
|
highlightField,
|
||||||
flashField,
|
flashField,
|
||||||
// theme, language, ...
|
// theme, language, ...
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ export function Icon({ type }) {
|
|||||||
clipRule="evenodd"
|
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]
|
}[type]
|
||||||
}
|
}
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -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}>
|
||||||
@@ -37,11 +63,14 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
|
|||||||
<button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
<button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||||
<Icon type="refresh" />
|
<Icon type="refresh" />
|
||||||
</button>
|
</button>
|
||||||
|
<button className={cx(styles.button)} onClick={() => stop(true)}>
|
||||||
|
<Icon type="stop" />
|
||||||
|
</button>
|
||||||
</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 theme={theme} value={code} onChange={setCode} onViewChanged={setView} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,8 +66,13 @@ function useCycle(props) {
|
|||||||
await Tone.start();
|
await Tone.start();
|
||||||
Tone.getTransport().start('+0.1');
|
Tone.getTransport().start('+0.1');
|
||||||
};
|
};
|
||||||
const stop = () => {
|
const stop = (setZero = false) => {
|
||||||
Tone.getTransport().pause();
|
if (setZero) {
|
||||||
|
Tone.getTransport().cancel();
|
||||||
|
Tone.getTransport().stop();
|
||||||
|
} else {
|
||||||
|
Tone.getTransport().pause();
|
||||||
|
}
|
||||||
setStarted(false);
|
setStarted(false);
|
||||||
};
|
};
|
||||||
const toggle = () => (started ? stop() : start());
|
const toggle = () => (started ? stop() : start());
|
||||||
|
|||||||
@@ -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/>.
|
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 } from 'react';
|
import { useCallback, useState, useMemo, useEffect } from 'react';
|
||||||
import { evaluate } from '@strudel.cycles/eval';
|
import { evaluate } from '@strudel.cycles/eval';
|
||||||
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
|
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
|
||||||
import useCycle from './useCycle.mjs';
|
import useCycle from './useCycle.mjs';
|
||||||
@@ -36,13 +36,15 @@ 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,
|
||||||
onEvent: useCallback(
|
onEvent: useCallback(
|
||||||
(time, event, currentTime) => {
|
(time, event, currentTime) => {
|
||||||
try {
|
try {
|
||||||
onEvent?.(event);
|
onEvent?.(time, event, currentTime);
|
||||||
if (event.context.logs?.length) {
|
if (event.context.logs?.length) {
|
||||||
event.context.logs.forEach(pushLog);
|
event.context.logs.forEach(pushLog);
|
||||||
}
|
}
|
||||||
@@ -51,8 +53,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
|
|||||||
if (defaultSynth) {
|
if (defaultSynth) {
|
||||||
const note = getPlayableNoteValue(event);
|
const note = getPlayableNoteValue(event);
|
||||||
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
||||||
} else {
|
} else if (!onEvent) {
|
||||||
throw new Error('no defaultSynth passed to useRepl.');
|
throw new Error(
|
||||||
|
'no defaultSynth nor onEvent passed to useRepl + event has no onTrigger. nothing happens',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
/* console.warn('no instrument chosen', event);
|
/* console.warn('no instrument chosen', event);
|
||||||
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
||||||
@@ -93,16 +97,16 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
|
|||||||
});
|
});
|
||||||
|
|
||||||
const activateCode = useCallback(
|
const activateCode = useCallback(
|
||||||
async (_code = code) => {
|
async (_code = code, evaluateOnly = false) => {
|
||||||
if (activeCode && !dirty) {
|
if (activeCode && !dirty) {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
cycle.start();
|
!evaluateOnly && cycle.start();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setPending(true);
|
setPending(true);
|
||||||
const parsed = await evaluate(_code);
|
const parsed = await evaluate(_code);
|
||||||
cycle.start();
|
!evaluateOnly && cycle.start();
|
||||||
broadcast({ type: 'start', from: id });
|
broadcast({ type: 'start', from: id });
|
||||||
setPattern(() => parsed.pattern);
|
setPattern(() => parsed.pattern);
|
||||||
if (autolink) {
|
if (autolink) {
|
||||||
@@ -135,7 +139,22 @@ 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 {
|
return {
|
||||||
|
hideHeader,
|
||||||
|
hideConsole,
|
||||||
pending,
|
pending,
|
||||||
code,
|
code,
|
||||||
setCode,
|
setCode,
|
||||||
@@ -146,8 +165,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
|
|||||||
dirty,
|
dirty,
|
||||||
log,
|
log,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
|
stop,
|
||||||
setActiveCode,
|
setActiveCode,
|
||||||
activateCode,
|
activateCode,
|
||||||
|
evaluateOnly,
|
||||||
activeCode,
|
activeCode,
|
||||||
pushLog,
|
pushLog,
|
||||||
hash,
|
hash,
|
||||||
|
|||||||
@@ -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,7 @@ export const materialPalenightTheme = EditorView.theme(
|
|||||||
backgroundColor: '#6199ff2f',
|
backgroundColor: '#6199ff2f',
|
||||||
},
|
},
|
||||||
|
|
||||||
'.cm-activeLine': { backgroundColor: highlightBackground },
|
'.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': {
|
||||||
|
|||||||
+24
-14
@@ -22,13 +22,17 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
flipTime = 0,
|
flipTime = 0,
|
||||||
flipValues = 0,
|
flipValues = 0,
|
||||||
hideNegative = false,
|
hideNegative = false,
|
||||||
inactive = '#C9E597',
|
// inactive = '#C9E597',
|
||||||
|
// inactive = '#FFCA28',
|
||||||
|
inactive = '#7491D2',
|
||||||
active = '#FFCA28',
|
active = '#FFCA28',
|
||||||
// background = '#2A3236',
|
// background = '#2A3236',
|
||||||
background = 'transparent',
|
background = 'transparent',
|
||||||
|
smear = 0,
|
||||||
|
playheadColor = 'white',
|
||||||
minMidi = 10,
|
minMidi = 10,
|
||||||
maxMidi = 90,
|
maxMidi = 90,
|
||||||
autorange = 0,
|
autorange = 1,
|
||||||
timeframe: timeframeProp,
|
timeframe: timeframeProp,
|
||||||
fold = 0,
|
fold = 0,
|
||||||
vertical = 0,
|
vertical = 0,
|
||||||
@@ -58,12 +62,14 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
flipTime && timeRange.reverse();
|
flipTime && timeRange.reverse();
|
||||||
flipValues && valueRange.reverse();
|
flipValues && valueRange.reverse();
|
||||||
|
|
||||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
|
||||||
this.draw(
|
this.draw(
|
||||||
(ctx, events, t) => {
|
(ctx, events, t) => {
|
||||||
ctx.fillStyle = background;
|
ctx.fillStyle = background;
|
||||||
ctx.clearRect(0, 0, w, h);
|
ctx.globalAlpha = 1; // reset!
|
||||||
ctx.fillRect(0, 0, w, h);
|
if (!smear) {
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
}
|
||||||
const inFrame = (event) =>
|
const inFrame = (event) =>
|
||||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
|
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
|
||||||
events.filter(inFrame).forEach((event) => {
|
events.filter(inFrame).forEach((event) => {
|
||||||
@@ -71,15 +77,6 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
ctx.fillStyle = event.context?.color || inactive;
|
ctx.fillStyle = event.context?.color || inactive;
|
||||||
ctx.strokeStyle = event.context?.color || active;
|
ctx.strokeStyle = event.context?.color || active;
|
||||||
ctx.globalAlpha = event.context.velocity ?? 1;
|
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);
|
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
const value = getValue(event);
|
const value = getValue(event);
|
||||||
@@ -107,6 +104,19 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
}
|
}
|
||||||
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
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,
|
from: from - overscan,
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ Pattern.prototype.tone = function (instrument) {
|
|||||||
} else if (instrument instanceof NoiseSynth) {
|
} else if (instrument instanceof NoiseSynth) {
|
||||||
instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
|
instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
|
||||||
} else if (instrument instanceof Piano) {
|
} else if (instrument instanceof Piano) {
|
||||||
note = getPlayableNoteValue(hap);
|
// note = getPlayableNoteValue(hap); // piano does not understand frequencies
|
||||||
|
note = hap.value;
|
||||||
|
if (typeof note === 'number') {
|
||||||
|
note = midi2note(note);
|
||||||
|
}
|
||||||
instrument.keyDown({ note, time, velocity });
|
instrument.keyDown({ note, time, velocity });
|
||||||
instrument.keyUp({ note, time: time + hap.duration.valueOf(), velocity });
|
instrument.keyUp({ note, time: time + hap.duration.valueOf(), velocity });
|
||||||
} else if (instrument instanceof Sampler) {
|
} else if (instrument instanceof Sampler) {
|
||||||
|
|||||||
+138
-134
@@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ This package adds [webdirt](https://github.com/dktr0/WebDirt) support to strudel
|
|||||||
|
|
||||||
Add default samples to repl:
|
Add default samples to repl:
|
||||||
|
|
||||||
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.
|
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/EmuSP12 > ../../repl/public/EmuSP12.json`
|
2. run `./makeSampleMap.sh ../../repl/public/samples/EmuSP12 > ../../repl/public/samples/EmuSP12.json`
|
||||||
3. adapt `loadWebDirt` in App.jsx + MiniRepl.jsx to use the generated json file
|
3. adapt `loadWebDirt` in App.jsx + MiniRepl.jsx to use the generated json file
|
||||||
|
|||||||
@@ -24,3 +24,5 @@ dist-ssr
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
oldtunes.mjs
|
oldtunes.mjs
|
||||||
|
|
||||||
|
public/samples/EMU World
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user