Compare commits

..

64 Commits

Author SHA1 Message Date
Felix Roos 235f3672aa meetup talk 2022-09-11 22:11:25 +02:00
Felix Roos 55dbcdde18 add new deck for meetup 2022-09-09 21:38:35 +02:00
Felix Roos d3f10518cc Merge branch 'talk' of https://github.com/tidalcycles/strudel into talk 2022-09-04 21:01:55 +02:00
Felix Roos 99fd5a53cd Merge remote-tracking branch 'origin/HEAD' into talk 2022-07-28 22:23:05 +02:00
Felix Roos 71dc108b2d Merge pull request #162 from larkob/euclidian
update to tutorial documentation
2022-07-28 19:26:41 +02:00
Lars Kobbe 82728be44b remove edit and pipe function (duplicate of layer and apply) 2022-07-25 22:20:06 +02:00
Lars Kobbe bebfa23cc9 add Euclidian, remove append 2022-07-24 21:20:38 +02:00
Felix Roos 7e21b683fb loadScript helper 2022-07-19 08:59:46 +02:00
Felix Roos a492628b33 more default samples (ignored wav files for now) 2022-07-10 20:59:07 +02:00
Felix Roos 53bb20c516 .out error handling 2022-07-10 20:55:05 +02:00
Felix Roos 588f74123e pianoroll optimizations
- fix flickering due to global alpha mutation
- change default inactive color
- add smear option
- add playheadColor option
2022-07-10 20:54:44 +02:00
Felix Roos e3473d5e70 demo ready slides 2022-07-10 20:53:27 +02:00
Felix Roos 8ce4de0c8d style errors, fixed backdrop + hide console 2022-07-10 20:52:46 +02:00
Felix Roos b87cea834f style fixes + hide-console option
- thicker highlighting outline
- only shade actual characters, not whole lines
- strudel hide-console will now hide console
2022-07-10 20:51:38 +02:00
Felix Roos 230ebe7439 talk now works offline too 2022-07-02 00:38:10 +02:00
Felix Roos e1f017916c small fixes 2022-07-02 00:11:57 +02:00
Felix Roos 6a445ee130 kind of finished slides 2022-07-01 23:55:04 +02:00
Felix Roos 8ea0044ffa Merge commit '85d3d7392ecfdc985fa8cc93e01846bec0f16dc4' into talk 2022-07-01 13:05:32 +02:00
Felix Roos 2d76de5906 log helpers 2022-07-01 10:56:31 +02:00
Felix Roos 4cce65c9fe now using symlinked samples folder from repl 2022-07-01 10:41:11 +02:00
Felix Roos 6b3687357e move samples to dedicated samples folder 2022-07-01 10:40:33 +02:00
Felix Roos 1d6e74fc77 small fix 2022-06-30 17:14:02 +02:00
Felix Roos c9fda97534 add prebake to maxirepl 2022-06-30 17:13:59 +02:00
Felix Roos 8796bcbdc3 fix: pianoroll 2022-06-30 17:13:47 +02:00
Felix Roos 4c66d9a553 Merge commit '8a1cd32d50e6689b14a77db0eca0b9033ab55312' into talk 2022-06-30 16:50:35 +02:00
Felix Roos 85d3d7392e hide-header magic comment 2022-06-30 16:49:24 +02:00
Felix Roos 31d0225341 toMidi now understands numbers 2022-06-30 16:49:01 +02:00
Felix Roos d0acb32497 piano nowalso understands n 2022-06-30 16:48:48 +02:00
Felix Roos aa9a32c9af add freq tunes from metro.html 2022-06-30 16:48:31 +02:00
Felix Roos a8610d0078 revert changing removeUndefineds 2022-06-30 02:26:24 +02:00
Felix Roos ef5dcd0dae fix: undefined error 2022-06-30 02:23:12 +02:00
Felix Roos ed86a94f99 add 2 tunes using new samples 2022-06-30 02:22:53 +02:00
Felix Roos 7be791f2d3 add some default samples 2022-06-30 02:22:38 +02:00
Felix Roos cc31a21b6f Merge remote-tracking branch 'origin/main' into talk 2022-06-22 20:21:55 +02:00
Felix Roos 72155d24c9 ec tal 2022-06-13 21:15:53 +02:00
Felix Roos 7672ca9971 small fixes 2022-06-07 22:34:39 +02:00
Felix Roos 07120e9e3b comment out stuff + style fix 2022-06-07 21:55:31 +02:00
Felix Roos 72feb3b65a pianoroll: do NOT round 2022-06-07 21:37:52 +02:00
Felix Roos 2f5ff44dc9 default to piano 2022-06-07 21:37:43 +02:00
Felix Roos 8051286904 react: usable onEvent 2022-06-07 21:16:17 +02:00
Felix Roos 8a57333f72 more slides 2022-06-07 20:40:29 +02:00
Felix Roos 512ba03fef can now hide slides 2022-06-07 20:38:51 +02:00
Felix Roos 4f0d2f181b more slides 2022-06-07 01:38:57 +02:00
Felix Roos 1339bc3b94 fix: piano now works with midi numbers 2022-06-07 01:38:12 +02:00
Felix Roos edf2fb9781 more slides + style 2022-06-06 22:39:51 +02:00
Felix Roos cf6bfd0e30 add clapping music 2022-06-06 19:10:48 +02:00
Felix Roos a93d6e884d repl: hard stop 2022-06-06 19:10:23 +02:00
Felix Roos 1a9b57e66c add phasing example 2022-06-06 13:20:17 +02:00
Felix Roos 0aec9423ac use new pianoroll settings 2022-06-05 23:33:08 +02:00
Felix Roos 54df4edca7 pianoroll: hideNegative flag 2022-06-05 23:32:10 +02:00
Felix Roos be6310b8e2 pianoroll: flipTime + flipValues 2022-06-05 23:24:59 +02:00
Felix Roos 3ab32114a6 replace from to with cycles / playhead 2022-06-05 22:51:59 +02:00
Felix Roos da85e1920b pianoroll: autorange + fold + vertical 2022-06-05 22:45:14 +02:00
Felix Roos 352d701b7b minirepl: highlighting can now be disabled 2022-06-05 22:44:24 +02:00
Felix Roos 349c1bc5d8 test new pianoroll 2022-06-04 23:55:26 +02:00
Felix Roos 9430caeb29 more flexible pianoroll 2022-06-04 23:55:05 +02:00
Felix Roos eb0adf4194 use evaluateOnly in MaxiRepl 2022-06-04 23:54:39 +02:00
Felix Roos 6fde3f2231 add evaluateOnly 2022-06-04 23:54:33 +02:00
Felix Roos 1c77e7e5b3 more content + style 2022-06-04 15:02:04 +02:00
Felix Roos 061abecaca added some slides 2022-06-04 01:01:59 +02:00
Felix Roos 98c131df9a cooler pattern 2022-06-02 22:48:18 +02:00
Felix Roos 1af10157d6 basic slides setup 2022-06-02 22:19:35 +02:00
Felix Roos a3eeaead2a remove odd packages 2022-06-02 22:18:06 +02:00
Felix Roos ff362da23f themable codemirror + repl 2022-06-02 22:17:50 +02:00
206 changed files with 11554 additions and 1190 deletions
+2 -1
View File
@@ -34,4 +34,5 @@ repl_old
tutorial.rendered.mdx
doc.json
talk/public/EmuSP12
talk/public/samples
talk/public/samples
server/samples/old
+24 -9
View File
@@ -1014,13 +1014,6 @@ 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;
@@ -1053,6 +1046,22 @@ 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..
@@ -1213,8 +1222,9 @@ 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 instanceof Pattern) {
const is = thing instanceof Pattern || thing?._Pattern;
if (thing?._Pattern && !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.
@@ -1557,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('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,6 +19,9 @@ 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 + '"');
@@ -31,6 +34,19 @@ 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
+83 -18
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 { EditorView, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
@@ -46,7 +46,13 @@ const materialPalenightTheme = EditorView.theme(
lineHeight: '22px',
},
'.cm-line': {
background: '#2C323699',
// background: '#2C323699',
background: 'transparent',
},
'.cm-line > *': {
// background: '#2C323699',
background: '#00000090',
// background: 'transparent',
},
// done
'&.cm-focused .cm-cursor': {
@@ -71,7 +77,7 @@ const materialPalenightTheme = EditorView.theme(
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-activeLine': { backgroundColor: cursor + '50' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
@@ -193,7 +199,7 @@ const highlightField = StateField.define({
if (from > l || to > l) {
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);
})).filter(Boolean), true);
}
@@ -205,7 +211,7 @@ const highlightField = StateField.define({
},
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, {
onViewChange: onViewChanged,
style: {
@@ -217,7 +223,7 @@ function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorD
onChange,
extensions: [
javascript(),
materialPalenight,
theme || materialPalenight,
highlightField,
flashField
]
@@ -288,8 +294,13 @@ function useCycle(props) {
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = () => {
Tone.getTransport().pause();
const stop = (setZero = false) => {
if (setZero) {
Tone.getTransport().cancel();
Tone.getTransport().stop();
} else {
Tone.getTransport().pause();
}
setStarted(false);
};
const toggle = () => (started ? stop() : start());
@@ -351,13 +362,15 @@ 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?.(event);
onEvent?.(time, event, currentTime);
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
@@ -366,8 +379,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
} else {
throw new Error('no defaultSynth passed to useRepl.');
} else if (!onEvent) {
throw new Error(
'no defaultSynth nor onEvent passed to useRepl + event has no onTrigger. nothing happens',
);
}
/* console.warn('no instrument chosen', 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(
async (_code = code) => {
async (_code = code, evaluateOnly = false) => {
if (activeCode && !dirty) {
setError(undefined);
cycle.start();
!evaluateOnly && cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
cycle.start();
!evaluateOnly && cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
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 {
hideHeader,
hideConsole,
pending,
code,
setCode,
@@ -459,8 +489,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
dirty,
log,
togglePlay,
stop,
setActiveCode,
activateCode,
evaluateOnly,
activeCode,
pushLog,
hash,
@@ -559,16 +591,25 @@ 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 }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
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
autolink: false,
onEvent
});
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01
@@ -580,7 +621,25 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
}
return isVisible || wasVisible.current;
}, [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", {
className: styles.container,
ref
@@ -598,11 +657,17 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
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,7 +60,8 @@ 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: 1px solid ${color}` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
return mark.range(from, to);
}),
)
@@ -78,7 +79,7 @@ const highlightField = StateField.define({
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 (
<>
<_CodeMirror
@@ -92,7 +93,7 @@ export default function CodeMirror({ value, onChange, onViewChanged, onCursor, o
onChange={onChange}
extensions={[
javascript(),
materialPalenight,
theme || materialPalenight,
highlightField,
flashField,
// theme, language, ...
+7
View File
@@ -26,6 +26,13 @@ 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>
+40 -11
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 useRepl from '../hooks/useRepl.mjs';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6 from './CodeMirror6';
import CodeMirror6, { flash } 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 }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
tune,
defaultSynth,
autolink: false,
});
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]);
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01,
@@ -26,7 +31,28 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
}
return isVisible || wasVisible.current;
}, [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 (
<div className={styles.container} ref={ref}>
<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()}>
<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 value={code} onChange={setCode} onViewChanged={setView} />}
<div className={styles.body}>
{show && <CodeMirror6 theme={theme} value={code} onChange={setCode} onViewChanged={setView} />}
</div>
</div>
);
+7 -2
View File
@@ -66,8 +66,13 @@ function useCycle(props) {
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = () => {
Tone.getTransport().pause();
const stop = (setZero = false) => {
if (setZero) {
Tone.getTransport().cancel();
Tone.getTransport().stop();
} else {
Tone.getTransport().pause();
}
setStarted(false);
};
const toggle = () => (started ? stop() : start());
+28 -7
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 } from 'react';
import { useCallback, useState, useMemo, useEffect } from 'react';
import { evaluate } from '@strudel.cycles/eval';
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
import useCycle from './useCycle.mjs';
@@ -36,13 +36,15 @@ 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?.(event);
onEvent?.(time, event, currentTime);
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
@@ -51,8 +53,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
} else {
throw new Error('no defaultSynth passed to useRepl.');
} else if (!onEvent) {
throw new Error(
'no defaultSynth nor onEvent passed to useRepl + event has no onTrigger. nothing happens',
);
}
/* console.warn('no instrument chosen', 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(
async (_code = code) => {
async (_code = code, evaluateOnly = false) => {
if (activeCode && !dirty) {
setError(undefined);
cycle.start();
!evaluateOnly && cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
cycle.start();
!evaluateOnly && cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
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 {
hideHeader,
hideConsole,
pending,
code,
setCode,
@@ -146,8 +165,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
dirty,
log,
togglePlay,
stop,
setActiveCode,
activateCode,
evaluateOnly,
activeCode,
pushLog,
hash,
@@ -36,7 +36,13 @@ export const materialPalenightTheme = EditorView.theme(
lineHeight: '22px',
},
'.cm-line': {
background: '#2C323699',
// background: '#2C323699',
background: 'transparent',
},
'.cm-line > *': {
// background: '#2C323699',
background: '#00000090',
// background: 'transparent',
},
// done
'&.cm-focused .cm-cursor': {
@@ -61,7 +67,7 @@ export const materialPalenightTheme = EditorView.theme(
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-activeLine': { backgroundColor: cursor + '50' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
+24 -14
View File
@@ -22,13 +22,17 @@ Pattern.prototype.pianoroll = function ({
flipTime = 0,
flipValues = 0,
hideNegative = false,
inactive = '#C9E597',
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 0,
autorange = 1,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
@@ -58,12 +62,14 @@ 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.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
ctx.globalAlpha = 1; // reset!
if (!smear) {
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) => {
@@ -71,15 +77,6 @@ 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);
@@ -107,6 +104,19 @@ 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,
+5 -1
View File
@@ -62,7 +62,11 @@ 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);
// note = getPlayableNoteValue(hap); // piano does not understand frequencies
note = hap.value;
if (typeof note === 'number') {
note = midi2note(note);
}
instrument.keyDown({ note, time, velocity });
instrument.keyUp({ note, time: time + hap.duration.valueOf(), velocity });
} else if (instrument instanceof Sampler) {
+138 -134
View File
@@ -126,146 +126,150 @@ const splitSN = (s, n) => {
Pattern.prototype.out = function () {
return this.onTrigger(async (t, hap, ct) => {
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
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);
}
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
if (typeof note === 'string') {
[note, n] = splitSN(note, n);
}
// 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);
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
}
} 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
// 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);
} 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);
// load sample
if (speed === 0) {
// no playback
return;
}
if (!s) {
console.warn('no sample specified');
return;
}
const soundfont = getSoundfontKey(s);
let bufferSource;
// 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();
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);
}
}
// 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]);
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);
}
});
};
+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` 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`
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`
3. adapt `loadWebDirt` in App.jsx + MiniRepl.jsx to use the generated json file
+1
View File
@@ -24,4 +24,5 @@ dist-ssr
*.sw?
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