mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-31 00:34:25 -04:00
Merge pull request #14 from tidalcycles/stateful-events
Stateful queries and events (WIP)
This commit is contained in:
+12
-12
@@ -18,7 +18,7 @@ try {
|
||||
console.warn('failed to decode', err);
|
||||
}
|
||||
// "balanced" | "interactive" | "playback";
|
||||
Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 }));
|
||||
// Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 }));
|
||||
const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.getDestination());
|
||||
defaultSynth.set({
|
||||
oscillator: { type: 'triangle' },
|
||||
@@ -37,11 +37,12 @@ const randomTune = getRandomTune();
|
||||
|
||||
function App() {
|
||||
const [editor, setEditor] = useState<any>();
|
||||
const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog } = useRepl({
|
||||
tune: decoded || randomTune,
|
||||
defaultSynth,
|
||||
onDraw: useCallback(markEvent(editor), [editor]),
|
||||
});
|
||||
const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog, pending } =
|
||||
useRepl({
|
||||
tune: decoded || randomTune,
|
||||
defaultSynth,
|
||||
onDraw: useCallback(markEvent(editor), [editor]),
|
||||
});
|
||||
const logBox = useRef<any>();
|
||||
// scroll log box to bottom when log changes
|
||||
useLayoutEffect(() => {
|
||||
@@ -51,12 +52,11 @@ function App() {
|
||||
// set active pattern on ctrl+enter
|
||||
useLayoutEffect(() => {
|
||||
// TODO: make sure this is only fired when editor has focus
|
||||
const handleKeyPress = (e: any) => {
|
||||
const handleKeyPress = async (e: any) => {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
switch (e.code) {
|
||||
case 'Enter':
|
||||
activateCode();
|
||||
!cycle.started && cycle.start();
|
||||
await activateCode();
|
||||
break;
|
||||
case 'Period':
|
||||
cycle.stop();
|
||||
@@ -88,11 +88,11 @@ function App() {
|
||||
</div>
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
const _code = getRandomTune();
|
||||
console.log('tune', _code); // uncomment this to debug when random code fails
|
||||
setCode(_code);
|
||||
const parsed = evaluate(_code);
|
||||
const parsed = await evaluate(_code);
|
||||
// Tone.Transport.cancel(Tone.Transport.seconds);
|
||||
setPattern(parsed.pattern);
|
||||
}}
|
||||
@@ -133,7 +133,7 @@ function App() {
|
||||
className="flex-none w-full border border-gray-700 p-2 bg-slate-700 hover:bg-slate-500"
|
||||
onClick={() => togglePlay()}
|
||||
>
|
||||
{cycle.started ? 'pause' : 'play'}
|
||||
{!pending ? <>{cycle.started ? 'pause' : 'play'}</> : <>loading...</>}
|
||||
</button>
|
||||
<textarea
|
||||
className="grow bg-[#283237] border-0 text-xs min-h-[200px]"
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function CodeMirror({ value, onChange, options, editorDidMount }:
|
||||
}
|
||||
|
||||
export const markEvent = (editor) => (time, event) => {
|
||||
const locs = event.value.locations;
|
||||
const locs = event.context.locations;
|
||||
if (!locs || !editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,10 +29,10 @@ hackLiteral(String, ['pure', 'p'], bootstrapped.pure); // comment out this line
|
||||
// this will add everything to global scope, which is accessed by eval
|
||||
Object.assign(globalThis, bootstrapped, Tone, toneHelpers);
|
||||
|
||||
export const evaluate: any = (code: string) => {
|
||||
export const evaluate: any = async (code: string) => {
|
||||
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
|
||||
// console.log('shapeshifted', shapeshifted);
|
||||
let evaluated = eval(shapeshifted);
|
||||
let evaluated = await eval(shapeshifted);
|
||||
if (typeof evaluated === 'function') {
|
||||
evaluated = evaluated();
|
||||
}
|
||||
|
||||
+2
-1
@@ -101,10 +101,11 @@ export function patternifyAST(ast: any): any {
|
||||
return ast.source_;
|
||||
}
|
||||
const { start, end } = ast.location_;
|
||||
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
||||
// return ast.source_;
|
||||
// the following line expects the shapeshifter to wrap this in withLocationOffset
|
||||
// because location_ is only relative to the mini string, but we need it relative to whole code
|
||||
return pure(ast.source_).withLocation({ start, end });
|
||||
return pure(value).withLocation({ start, end });
|
||||
}
|
||||
return patternifyAST(ast.source_);
|
||||
case 'stretch':
|
||||
|
||||
@@ -92,15 +92,16 @@ export default (code) => {
|
||||
// add to location to pure(x) calls
|
||||
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
|
||||
const literal = node.arguments[0];
|
||||
const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
|
||||
return reifyWithLocation(value + '', node.arguments[0], ast.locations, artificialNodes);
|
||||
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
|
||||
// console.log('value',value);
|
||||
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
|
||||
}
|
||||
// replace pseudo note variables
|
||||
if (node.type === 'IdentifierExpression') {
|
||||
if (isNote(node.name)) {
|
||||
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
|
||||
if (addLocations && isMarkable) {
|
||||
return reifyWithLocation(value, node, ast.locations, artificialNodes);
|
||||
return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes);
|
||||
}
|
||||
return new LiteralStringExpression({ value });
|
||||
}
|
||||
@@ -110,7 +111,7 @@ export default (code) => {
|
||||
}
|
||||
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) {
|
||||
// console.log('add', node);
|
||||
return reifyWithLocation(node.value, node, ast.locations, artificialNodes);
|
||||
return reifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||
}
|
||||
if (!addMiniLocations) {
|
||||
return wrapFunction('reify', node);
|
||||
@@ -219,10 +220,10 @@ function wrapLocationOffset(node, stringNode, locations, artificialNodes) {
|
||||
|
||||
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||
function reifyWithLocation(value, node, locations, artificialNodes) {
|
||||
function reifyWithLocation(literalNode, node, locations, artificialNodes) {
|
||||
const withLocation = new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
object: wrapFunction('reify', new LiteralStringExpression({ value })),
|
||||
object: wrapFunction('reify', literalNode),
|
||||
property: 'withLocation',
|
||||
}),
|
||||
arguments: [getLocationObject(node, locations)],
|
||||
|
||||
+11
-34
@@ -3,21 +3,6 @@ import { Pattern as _Pattern } from '../../strudel.mjs';
|
||||
|
||||
const Pattern = _Pattern as any;
|
||||
|
||||
export declare interface NoteEvent {
|
||||
value: string | number;
|
||||
scale?: string;
|
||||
}
|
||||
|
||||
function toNoteEvent(event: string | NoteEvent): NoteEvent {
|
||||
if (typeof event === 'string' || typeof event === 'number') {
|
||||
return { value: event };
|
||||
}
|
||||
if (event.value) {
|
||||
return event;
|
||||
}
|
||||
throw new Error('not a valid note event: ' + JSON.stringify(event));
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -60,24 +45,16 @@ function scaleTranspose(scale: string, offset: number, note: string) {
|
||||
return n + o;
|
||||
}
|
||||
|
||||
Pattern.prototype._mapNotes = function (func: (note: NoteEvent) => NoteEvent) {
|
||||
return this.fmap((event: string | NoteEvent) => {
|
||||
const noteEvent = toNoteEvent(event);
|
||||
// TODO: generalize? this is practical for any event that is expected to be an object with
|
||||
return { ...noteEvent, ...func(noteEvent) };
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
|
||||
return this._mapNotes(({ value, scale }: NoteEvent) => {
|
||||
return this._withEvent((event) => {
|
||||
const interval = !isNaN(Number(intervalOrSemitones))
|
||||
? Interval.fromSemitones(intervalOrSemitones as number)
|
||||
: String(intervalOrSemitones);
|
||||
if (typeof value === 'number') {
|
||||
if (typeof event.value === 'number') {
|
||||
const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval;
|
||||
return { value: value + semitones };
|
||||
return event.withValue(() => event.value + semitones);
|
||||
}
|
||||
return { value: Note.transpose(value, interval), scale };
|
||||
return event.withValue(() => Note.transpose(event.value, interval));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -88,26 +65,26 @@ Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
|
||||
// or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or
|
||||
|
||||
Pattern.prototype._scaleTranspose = function (offset: number | string) {
|
||||
return this._mapNotes(({ value, scale }: NoteEvent) => {
|
||||
if (!scale) {
|
||||
return this._withEvent((event) => {
|
||||
if (!event.context.scale) {
|
||||
throw new Error('can only use scaleTranspose after .scale');
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
if (typeof event.value !== 'string') {
|
||||
throw new Error('can only use scaleTranspose with notes');
|
||||
}
|
||||
return { value: scaleTranspose(scale, Number(offset), value), scale };
|
||||
return event.withValue(() => scaleTranspose(event.context.scale, Number(offset), event.value));
|
||||
});
|
||||
};
|
||||
Pattern.prototype._scale = function (scale: string) {
|
||||
return this._mapNotes((value) => {
|
||||
let note = value.value;
|
||||
return this._withEvent((event) => {
|
||||
let note = event.value;
|
||||
const asNumber = Number(note);
|
||||
if (!isNaN(asNumber)) {
|
||||
let [tonic, scaleName] = Scale.tokenize(scale);
|
||||
const { pc, oct = 3 } = Note.get(tonic);
|
||||
note = scaleTranspose(pc + ' ' + scaleName, asNumber, pc + oct);
|
||||
}
|
||||
return { ...value, value: note, scale };
|
||||
return event.withValue(() => note).setContext({ ...event.context, scale });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+31
-25
@@ -18,6 +18,7 @@ import {
|
||||
Sampler,
|
||||
getDestination
|
||||
} from 'tone';
|
||||
import { Piano } from '@tonejs/piano';
|
||||
|
||||
// what about
|
||||
// https://www.charlie-roberts.com/gibberish/playground/
|
||||
@@ -27,18 +28,20 @@ const Pattern = _Pattern as any;
|
||||
// with this function, you can play the pattern with any tone synth
|
||||
Pattern.prototype.tone = function (instrument) {
|
||||
// instrument.toDestination();
|
||||
return this.fmap((value: any) => {
|
||||
value = typeof value !== 'object' && !Array.isArray(value) ? { value } : value;
|
||||
return this._withEvent((event) => {
|
||||
const onTrigger = (time, event) => {
|
||||
if (instrument.constructor.name === 'PluckSynth') {
|
||||
instrument.triggerAttack(value.value, time);
|
||||
instrument.triggerAttack(event.value, time);
|
||||
} else if (instrument.constructor.name === 'NoiseSynth') {
|
||||
instrument.triggerAttackRelease(event.duration, time); // noise has no value
|
||||
} else if (instrument.constructor.name === 'Piano') {
|
||||
instrument.keyDown({ note: event.value, time, velocity: 0.5 });
|
||||
instrument.keyUp({ note: event.value, time: time + event.duration });
|
||||
} else {
|
||||
instrument.triggerAttackRelease(value.value, event.duration, time);
|
||||
instrument.triggerAttackRelease(event.value, event.duration, time);
|
||||
}
|
||||
};
|
||||
return { ...value, instrument, onTrigger };
|
||||
return event.setContext({ ...event.context, instrument, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -56,6 +59,11 @@ export const pluck = (options) => new PluckSynth(options);
|
||||
export const polysynth = (options) => new PolySynth(options);
|
||||
export const sampler = (options) => new Sampler(options);
|
||||
export const synth = (options) => new Synth(options);
|
||||
export const piano = async (options = { velocities: 1 }) => {
|
||||
const p = new Piano(options);
|
||||
await p.load();
|
||||
return p;
|
||||
};
|
||||
|
||||
// effect helpers
|
||||
export const vol = (v) => new Gain(v);
|
||||
@@ -106,13 +114,12 @@ Pattern.prototype._poly = function (type: any = 'triangle') {
|
||||
// this.instrument = new PolySynth(Synth, instrumentConfig).toDestination();
|
||||
this.instrument = poly(type);
|
||||
}
|
||||
return this.fmap((value: any) => {
|
||||
value = typeof value !== 'object' && !Array.isArray(value) ? { value } : value;
|
||||
return this._withEvent((event: any) => {
|
||||
const onTrigger = (time, event) => {
|
||||
this.instrument.set(instrumentConfig);
|
||||
this.instrument.triggerAttackRelease(value.value, event.duration, time);
|
||||
this.instrument.triggerAttackRelease(event.value, event.duration, time);
|
||||
};
|
||||
return { ...value, instrumentConfig, onTrigger };
|
||||
return event.setContext({ ...event.context, instrumentConfig, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -139,8 +146,7 @@ const getTrigger = (getChain: any, value: any) => (time: number, event: any) =>
|
||||
};
|
||||
|
||||
Pattern.prototype._synth = function (type: any = 'triangle') {
|
||||
return this.fmap((value: any) => {
|
||||
value = typeof value !== 'object' && !Array.isArray(value) ? { value } : value;
|
||||
return this._withEvent((event: any) => {
|
||||
const instrumentConfig: any = {
|
||||
oscillator: { type },
|
||||
envelope: { attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01 },
|
||||
@@ -150,39 +156,39 @@ Pattern.prototype._synth = function (type: any = 'triangle') {
|
||||
instrument.set(instrumentConfig);
|
||||
return instrument;
|
||||
};
|
||||
const onTrigger = getTrigger(() => getInstrument().toDestination(), value.value);
|
||||
return { ...value, getInstrument, instrumentConfig, onTrigger };
|
||||
const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
|
||||
return event.setContext({ ...event.context, getInstrument, instrumentConfig, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.adsr = function (attack = 0.01, decay = 0.01, sustain = 0.6, release = 0.01) {
|
||||
return this.fmap((value: any) => {
|
||||
if (!value?.getInstrument) {
|
||||
return this._withEvent((event: any) => {
|
||||
if (!event.context.getInstrument) {
|
||||
throw new Error('cannot chain adsr: need instrument first (like synth)');
|
||||
}
|
||||
const instrumentConfig = { ...value.instrumentConfig, envelope: { attack, decay, sustain, release } };
|
||||
const instrumentConfig = { ...event.context.instrumentConfig, envelope: { attack, decay, sustain, release } };
|
||||
const getInstrument = () => {
|
||||
const instrument = value.getInstrument();
|
||||
const instrument = event.context.getInstrument();
|
||||
instrument.set(instrumentConfig);
|
||||
return instrument;
|
||||
};
|
||||
const onTrigger = getTrigger(() => getInstrument().toDestination(), value.value);
|
||||
return { ...value, getInstrument, instrumentConfig, onTrigger };
|
||||
const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
|
||||
return event.setContext({ ...event.context, getInstrument, instrumentConfig, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.chain = function (...effectGetters: any) {
|
||||
return this.fmap((value: any) => {
|
||||
if (!value?.getInstrument) {
|
||||
return this._withEvent((event: any) => {
|
||||
if (!event.context?.getInstrument) {
|
||||
throw new Error('cannot chain: need instrument first (like synth)');
|
||||
}
|
||||
const chain = (value.chain || []).concat(effectGetters);
|
||||
const chain = (event.context.chain || []).concat(effectGetters);
|
||||
const getChain = () => {
|
||||
const effects = chain.map((getEffect: any) => getEffect());
|
||||
return value.getInstrument().chain(...effects, getDestination());
|
||||
return event.context.getInstrument().chain(...effects, getDestination());
|
||||
};
|
||||
const onTrigger = getTrigger(getChain, value.value);
|
||||
return { ...value, getChain, onTrigger, chain };
|
||||
const onTrigger = getTrigger(getChain, event.value);
|
||||
return event.setContext({ ...event.context, getChain, onTrigger, chain });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+9
-2
@@ -309,7 +309,6 @@ export const loungerave = `() => {
|
||||
//.early("0.25 0");
|
||||
}`;
|
||||
|
||||
|
||||
export const caverave = `() => {
|
||||
const delay = new FeedbackDelay(1/8, .4).chain(vol(0.5), out());
|
||||
const kick = new MembraneSynth().chain(vol(.8), out());
|
||||
@@ -342,7 +341,6 @@ export const caverave = `() => {
|
||||
).slow(2);
|
||||
}`;
|
||||
|
||||
|
||||
export const callcenterhero = `()=>{
|
||||
const bpm = 90;
|
||||
const lead = polysynth().set({...osc('sine4'),...adsr(.004)}).chain(vol(0.15),out())
|
||||
@@ -451,3 +449,12 @@ export const sowhatelse = `()=> {
|
||||
"[2,4]/4".scale('D dorian').apply(t).tone(instr('pad')).mask("<x x x ~>/8")
|
||||
).fast(6/8)
|
||||
}`;
|
||||
|
||||
export const barryHarris = `piano()
|
||||
.then(p => "0,2,[7 6]"
|
||||
.add("<0 1 2 3 4 5 7 8>")
|
||||
.scale('C bebop major')
|
||||
.transpose("<0 1 2 1>/8")
|
||||
.slow(2)
|
||||
.tone(p.toDestination()))
|
||||
`;
|
||||
|
||||
Vendored
+1
@@ -15,6 +15,7 @@ export declare interface Hap<T = any> {
|
||||
whole: TimeSpan;
|
||||
part: TimeSpan;
|
||||
value: T;
|
||||
context: any;
|
||||
show: () => string;
|
||||
}
|
||||
export declare interface Pattern<T = any> {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { ToneEventCallback } from 'tone';
|
||||
import * as Tone from 'tone';
|
||||
import { TimeSpan } from '../../strudel.mjs';
|
||||
import { TimeSpan, State } from '../../strudel.mjs';
|
||||
import type { Hap } from './types';
|
||||
|
||||
export declare interface UseCycleProps {
|
||||
onEvent: ToneEventCallback<any>;
|
||||
onQuery?: (query: TimeSpan) => Hap[];
|
||||
onQuery?: (state: State) => Hap[];
|
||||
onSchedule?: (events: Hap[], cycle: number) => void;
|
||||
onDraw?: ToneEventCallback<any>;
|
||||
ready?: boolean; // if false, query will not be called on change props
|
||||
@@ -22,7 +22,7 @@ function useCycle(props: UseCycleProps) {
|
||||
// pull events with onQuery + count up to next cycle
|
||||
const query = (cycle = activeCycle()) => {
|
||||
const timespan = new TimeSpan(cycle, cycle + 1);
|
||||
const events = onQuery?.(timespan) || [];
|
||||
const events = onQuery?.(new State(timespan)) || [];
|
||||
onSchedule?.(events, cycle);
|
||||
// cancel events after current query. makes sure no old events are player for rescheduled cycles
|
||||
// console.log('schedule', cycle);
|
||||
@@ -47,6 +47,7 @@ function useCycle(props: UseCycleProps) {
|
||||
time: event.part.begin.valueOf(),
|
||||
duration: event.whole.end.sub(event.whole.begin).valueOf(),
|
||||
value: event.value,
|
||||
context: event.context,
|
||||
};
|
||||
onEvent(time, toneEvent);
|
||||
Tone.Draw.schedule(() => {
|
||||
|
||||
+15
-10
@@ -17,19 +17,22 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
const [activeCode, setActiveCode] = useState<string>();
|
||||
const [log, setLog] = useState('');
|
||||
const [error, setError] = useState<Error>();
|
||||
const [pending, setPending] = useState(false);
|
||||
const [hash, setHash] = useState('');
|
||||
const [pattern, setPattern] = useState<Pattern>();
|
||||
const dirty = code !== activeCode || error;
|
||||
const generateHash = () => encodeURIComponent(btoa(code));
|
||||
const activateCode = (_code = code) => {
|
||||
!cycle.started && cycle.start();
|
||||
broadcast({ type: 'start', from: id });
|
||||
const activateCode = async (_code = code) => {
|
||||
if (activeCode && !dirty) {
|
||||
setError(undefined);
|
||||
!cycle.started && cycle.start();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = evaluate(_code);
|
||||
setPending(true);
|
||||
const parsed = await evaluate(_code);
|
||||
!cycle.started && cycle.start();
|
||||
broadcast({ type: 'start', from: id });
|
||||
setPattern(() => parsed.pattern);
|
||||
if (autolink) {
|
||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
||||
@@ -37,6 +40,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
setHash(generateHash());
|
||||
setError(undefined);
|
||||
setActiveCode(_code);
|
||||
setPending(false);
|
||||
} catch (err: any) {
|
||||
err.message = 'evaluation error: ' + err.message;
|
||||
console.warn(err);
|
||||
@@ -47,7 +51,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
// logs events of cycle
|
||||
const logCycle = (_events: any, cycle: any) => {
|
||||
if (_events.length) {
|
||||
//pushLog(`# cycle ${cycle}\n` + _events.map((e: any) => e.show()).join('\n'));
|
||||
// pushLog(`# cycle ${cycle}\n` + _events.map((e: any) => e.show()).join('\n'));
|
||||
}
|
||||
};
|
||||
// cycle hook to control scheduling
|
||||
@@ -57,8 +61,9 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
(time, event) => {
|
||||
try {
|
||||
onEvent?.(event);
|
||||
if (!event.value?.onTrigger) {
|
||||
const note = event.value?.value || event.value;
|
||||
const { onTrigger } = event.context;
|
||||
if (!onTrigger) {
|
||||
const note = event.value;
|
||||
if (!isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
@@ -70,7 +75,6 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
/* console.warn('no instrument chosen', event);
|
||||
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
||||
} else {
|
||||
const { onTrigger } = event.value;
|
||||
onTrigger(time, event);
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -82,9 +86,9 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
[onEvent]
|
||||
),
|
||||
onQuery: useCallback(
|
||||
(span) => {
|
||||
(state) => {
|
||||
try {
|
||||
return pattern?.query(span) || [];
|
||||
return pattern?.query(state) || [];
|
||||
} catch (err: any) {
|
||||
console.warn(err);
|
||||
err.message = 'query error: ' + err.message;
|
||||
@@ -146,6 +150,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
};
|
||||
|
||||
return {
|
||||
pending,
|
||||
code,
|
||||
setCode,
|
||||
pattern,
|
||||
|
||||
@@ -19,7 +19,7 @@ Pattern.prototype.fmapNested = function (func) {
|
||||
.map((event) =>
|
||||
reify(func(event))
|
||||
.query(span)
|
||||
.map((hap) => new Hap(event.whole, event.part, hap.value))
|
||||
.map((hap) => new Hap(event.whole, event.part, hap.value, hap.context))
|
||||
)
|
||||
.flat()
|
||||
);
|
||||
@@ -32,17 +32,18 @@ Pattern.prototype.voicings = function (range) {
|
||||
range = ['F3', 'A4'];
|
||||
}
|
||||
return this.fmapNested((event) => {
|
||||
lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range);
|
||||
return stack(...lastVoicing);
|
||||
lastVoicing = getVoicing(event.value, lastVoicing, range);
|
||||
return stack(...lastVoicing)._withContext(() => ({
|
||||
locations: event.context.locations || [],
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.rootNotes = function (octave = 2) {
|
||||
// range = ['G1', 'C3']
|
||||
return this._mapNotes((value) => {
|
||||
const [_, root] = value.value.match(/^([a-gA-G])[b#]?.*$/);
|
||||
const bassNote = root + octave;
|
||||
return { ...value, value: bassNote };
|
||||
return this.fmap((value) => {
|
||||
const [_, root] = value.match(/^([a-gA-G])[b#]?.*$/);
|
||||
return root + octave;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user