Merge pull request #14 from tidalcycles/stateful-events

Stateful queries and events (WIP)
This commit is contained in:
Felix Roos 2022-02-27 20:52:51 +01:00 committed by GitHub
commit 190729df73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 383 additions and 249 deletions

33
repl/package-lock.json generated
View File

@ -6,6 +6,7 @@
"": {
"dependencies": {
"@tonaljs/tonal": "^4.6.5",
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1",
"codemirror": "^5.65.1",
"estraverse": "^5.3.0",
@ -3023,6 +3024,23 @@
"@tonaljs/time-signature": "^4.6.2"
}
},
"node_modules/@tonejs/piano": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@tonejs/piano/-/piano-0.2.1.tgz",
"integrity": "sha512-JIwZ91RSFR7Rt16o7cA7O7G30wenFl0lY5yhTsuwZmn48MO9KV+X7kyXE98Bqvs/dCBVg9PoAJ1GKMabPOW4yQ==",
"dependencies": {
"tslib": "^1.11.1"
},
"peerDependencies": {
"tone": "^14.6.1",
"webmidi": "^2.5.1"
}
},
"node_modules/@tonejs/piano/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@ -14482,6 +14500,21 @@
"@tonaljs/time-signature": "^4.6.2"
}
},
"@tonejs/piano": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@tonejs/piano/-/piano-0.2.1.tgz",
"integrity": "sha512-JIwZ91RSFR7Rt16o7cA7O7G30wenFl0lY5yhTsuwZmn48MO9KV+X7kyXE98Bqvs/dCBVg9PoAJ1GKMabPOW4yQ==",
"requires": {
"tslib": "^1.11.1"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",

View File

@ -1,6 +1,6 @@
{
"scripts": {
"start": "snowpack dev",
"start": "snowpack dev --polyfill-node",
"build": "snowpack build && cp ./public/.nojekyll ../docs && npm run build-tutorial",
"static": "npx serve ../docs",
"test": "web-test-runner \"src/**/*.test.tsx\"",
@ -12,6 +12,7 @@
},
"dependencies": {
"@tonaljs/tonal": "^4.6.5",
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1",
"codemirror": "^5.65.1",
"estraverse": "^5.3.0",

View File

@ -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]"

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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':

View File

@ -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)],

View File

@ -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 });
});
};

View File

@ -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 });
});
};

View File

@ -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()))
`;

1
repl/src/types.d.ts vendored
View File

@ -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> {

View File

@ -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(() => {

View File

@ -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,

View File

@ -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;
});
};

View File

@ -172,23 +172,29 @@ class Hap {
then the whole will be returned as None, in which case the given
value will have been sampled from the point halfway between the
start and end of the 'part' timespan.
The context is to store a list of source code locations causing the event
*/
constructor(whole, part, value) {
constructor(whole, part, value, context = {}, stateful = false) {
this.whole = whole
this.part = part
this.value = value
this.context = context
this.stateful = stateful
if (stateful) {
assert(typeof this.value === "function", "Stateful values must be functions");
}
}
withSpan(func) {
// Returns a new event with the function f applies to the event timespan.
const whole = this.whole ? func(this.whole) : undefined
return new Hap(whole, func(this.part), this.value)
return new Hap(whole, func(this.part), this.value, this.context)
}
withValue(func) {
// Returns a new event with the function f applies to the event value.
return new Hap(this.whole, this.part, func(this.value))
return new Hap(this.whole, this.part, func(this.value), this.context)
}
hasOnset() {
@ -197,6 +203,15 @@ class Hap {
return (this.whole != undefined) && (this.whole.begin.equals(this.part.begin))
}
resolveState(state) {
if (this.stateful && this.hasOnset()) {
const func = this.value
[newState, newValue] = func(state)
return [newState, this.withValue(() => newValue)]
}
return [state, this]
}
spanEquals(other) {
return((this.whole == undefined && other.whole == undefined)
|| this.whole.equals(other.whole)
@ -212,7 +227,32 @@ class Hap {
}
show() {
return "(" + (this.whole == undefined ? "~" : this.whole.show()) + ", " + this.part.show() + ", " + JSON.stringify(this.value?.value ?? this.value) + ")"
return "(" + (this.whole == undefined ? "~" : this.whole.show()) + ", " + this.part.show() + ", " + this.value + ")"
}
setContext(context) {
return new Hap(this.whole, this.part, this.value, context)
}
}
export class State {
constructor(span, controls={}) {
this.span = span
this.controls = controls
}
// Returns new State with different span
setSpan(span) {
return new State(span, this.controls)
}
withSpan(func) {
return this.setSpan(func(this.span))
}
// Returns new State with different controls
setControls(controls) {
return new State(this.span, controls)
}
}
@ -244,25 +284,27 @@ class Pattern {
// easier to express, as all events are then constrained to happen within
// a cycle.
const pat = this
const q = span => flatten(span.spanCycles.map(subspan => pat.query(subspan)))
const q = state => {
return flatten(state.span.spanCycles.map(subspan => pat.query(state.setSpan(subspan))))
}
return new Pattern(q)
}
withQuerySpan(func) {
return new Pattern(span => this.query(func(span)))
return new Pattern(state => this.query(state.withSpan(func)))
}
withQueryTime(func) {
// Returns a new pattern, with the function applied to both the begin
// and end of the the query timespan
return new Pattern(span => this.query(span.withTime(func)))
return new Pattern(state => this.query(state.withSpan(span => span.withTime(func))))
}
withEventSpan(func) {
// Returns a new pattern, with the function applied to each event
// timespan.
return new Pattern(span => this.query(span).map(hap => hap.withSpan(func)))
return new Pattern(state => this.query(state).map(hap => hap.withSpan(func)))
}
withEventTime(func) {
@ -272,21 +314,36 @@ class Pattern {
}
_withEvents(func) {
return new Pattern(span => func(this.query(span)))
return new Pattern(state => func(this.query(state)))
}
_withEvent(func) {
return this._withEvents(events => events.map(func))
}
_setContext(context) {
return this._withEvent(event => event.setContext(context))
}
_withContext(func) {
return this._withEvent(event => event.setContext(func(event.context)))
}
_stripContext() {
return this._withEvent(event => event.setContext({}))
}
withLocation(location) {
return this.fmap(value => {
value = typeof value === 'object' && !Array.isArray(value) ? value : { value };
const locations = (value.locations || []).concat([location]);
return {...value, locations }
})
return this._withContext((context) => {
const locations = (context.locations || []).concat([location])
return { ...context, locations }
});
}
withValue(func) {
// Returns a new pattern, with the function applied to the value of
// each event. It has the alias 'fmap'.
return new Pattern(span => this.query(span).map(hap => hap.withValue(func)))
return new Pattern(state => this.query(state).map(hap => hap.withValue(func)))
}
// alias
@ -295,11 +352,11 @@ class Pattern {
}
_filterEvents(event_test) {
return new Pattern(span => this.query(span).filter(event_test))
return new Pattern(state => this.query(state).filter(event_test))
}
_filterValues(value_test) {
return new Pattern(span => this.query(span).filter(hap => value_test(hap.value)))
return new Pattern(state => this.query(state).filter(hap => value_test(hap.value)))
}
_removeUndefineds() {
@ -318,20 +375,21 @@ class Pattern {
// resolve wholes, applies a given pattern of values to that
// pattern of functions.
const pat_func = this
const query = function(span) {
const event_funcs = pat_func.query(span)
const event_vals = pat_val.query(span)
const query = function(state) {
const event_funcs = pat_func.query(state)
const event_vals = pat_val.query(state)
const apply = function(event_func, event_val) {
const s = event_func.part.intersection(event_val.part)
if (s == undefined) {
return undefined
}
return new Hap(whole_func(event_func.whole, event_val.whole), s, event_func.value(event_val.value))
// TODO: is it right to add event_val.context here?
return new Hap(whole_func(event_func.whole, event_val.whole), s, event_func.value(event_val.value), event_val.context)
}
return flatten(event_funcs.map(event_func => removeUndefineds(event_vals.map(event_val => apply(event_func, event_val)))))
}
return new Pattern(query)
}
}
appBoth(pat_val) {
// Tidal's <*>
@ -347,15 +405,19 @@ class Pattern {
appLeft(pat_val) {
const pat_func = this
const query = function(span) {
const query = function(state) {
const haps = []
for (const hap_func of pat_func.query(span)) {
const event_vals = pat_val.query(hap_func.part)
for (const hap_func of pat_func.query(state)) {
const event_vals = pat_val.query(state.setSpan(hap_func.part))
for (const hap_val of event_vals) {
const new_whole = hap_func.whole
const new_part = hap_func.part.intersection_e(hap_val.part)
const new_value = hap_func.value(hap_val.value)
const hap = new Hap(new_whole, new_part, new_value)
const hap = new Hap(new_whole, new_part, new_value, {
...hap_val.context,
...hap_func.context,
locations: (hap_val.context.locations || []).concat(hap_func.context.locations || []),
});
haps.push(hap)
}
}
@ -367,15 +429,19 @@ class Pattern {
appRight(pat_val) {
const pat_func = this
const query = function(span) {
const query = function(state) {
const haps = []
for (const hap_val of pat_val.query(span)) {
const hap_funcs = pat_func.query(hap_val.part)
for (const hap_val of pat_val.query(state)) {
const hap_funcs = pat_func.query(state.setSpan(hap_val.part))
for (const hap_func of hap_funcs) {
const new_whole = hap_val.whole
const new_part = hap_func.part.intersection_e(hap_val.part)
const new_value = hap_func.value(hap_val.value)
const hap = new Hap(new_whole, new_part, new_value)
const hap = new Hap(new_whole, new_part, new_value, {
...hap_func.context,
...hap_val.context,
locations: (hap_val.context.locations || []).concat(hap_func.context.locations || []),
})
haps.push(hap)
}
}
@ -384,8 +450,12 @@ class Pattern {
return new Pattern(query)
}
get firstCycle() {
return this.query(new TimeSpan(Fraction(0), Fraction(1)))
firstCycle(with_context=false) {
var self = this
if (!with_context) {
self = self._stripContext()
}
return self.query(new State(new TimeSpan(Fraction(0), Fraction(1))))
}
_sortEventsByPart() {
@ -418,16 +488,18 @@ class Pattern {
_bindWhole(choose_whole, func) {
const pat_val = this
const query = function(span) {
const query = function(state) {
const withWhole = function(a, b) {
return new Hap(choose_whole(a.whole, b.whole), b.part,
b.value
)
return new Hap(choose_whole(a.whole, b.whole), b.part, b.value, {
...a.context,
...b.context,
locations: (a.context.locations || []).concat(b.context.locations || []),
});
}
const match = function (a) {
return func(a.value).query(a.part).map(b => withWhole(a, b))
return func(a.value).query(state.setSpan(a.part)).map(b => withWhole(a, b))
}
return flatten(pat_val.query(span).map(a => match(a)))
return flatten(pat_val.query(state).map(a => match(a)))
}
return new Pattern(query)
}
@ -590,7 +662,8 @@ class Pattern {
rev() {
const pat = this
const query = function(span) {
const query = function(state) {
const span = state.span
const cycle = span.begin.sam()
const next_cycle = span.begin.nextSam()
const reflect = function(to_reflect) {
@ -601,7 +674,7 @@ class Pattern {
reflected.end = tmp
return reflected
}
const haps = pat.query(reflect(span))
const haps = pat.query(state.setSpan(reflect(span)))
return haps.map(hap => hap.withSpan(reflect))
}
return new Pattern(query)._splitQueries()
@ -679,8 +752,8 @@ const silence = new Pattern(_ => [])
function pure(value) {
// A discrete value that repeats once per cycle
function query(span) {
return span.spanCycles.map(subspan => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value))
function query(state) {
return state.span.spanCycles.map(subspan => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value))
}
return new Pattern(query)
}
@ -691,7 +764,7 @@ function steady(value) {
}
export const signal = func => {
const query = span => [new Hap(undefined, span, func(span.midpoint()))]
const query = state => [new Hap(undefined, state.span, func(state.span.midpoint()))]
return new Pattern(query)
}
@ -728,7 +801,7 @@ function reify(thing) {
function stack(...pats) {
const reified = pats.map(pat => reify(pat))
const query = span => flatten(reified.map(pat => pat.query(span)))
const query = state => flatten(reified.map(pat => pat.query(state)))
return new Pattern(query)
}
@ -736,7 +809,8 @@ function slowcat(...pats) {
// Concatenation: combines a list of patterns, switching between them
// successively, one per cycle.
pats = pats.map(reify)
const query = function(span) {
const query = function(state) {
const span = state.span
const pat_n = Math.floor(span.begin) % pats.length;
const pat = pats[pat_n]
if (!pat) {
@ -747,7 +821,7 @@ function slowcat(...pats) {
// For example if three patterns are slowcat-ed, the fourth cycle of the result should
// be the second (rather than fourth) cycle from the first pattern.
const offset = span.begin.floor().sub(span.begin.div(pats.length).floor())
return pat.withEventTime(t => t.add(offset)).query(span.withTime(t => t.sub(offset)))
return pat.withEventTime(t => t.add(offset)).query(state.setSpan(span.withTime(t => t.sub(offset))))
}
return new Pattern(query)._splitQueries()
}
@ -756,10 +830,10 @@ function slowcatPrime(...pats) {
// Concatenation: combines a list of patterns, switching between them
// successively, one per cycle. Unlike slowcat, this version will skip cycles.
pats = pats.map(reify)
const query = function(span) {
const pat_n = Math.floor(span.begin) % pats.length
const query = function(state) {
const pat_n = Math.floor(state.span.begin) % pats.length
const pat = pats[pat_n]
return pat.query(span)
return pat.query(state)
}
return new Pattern(query)._splitQueries()
}
@ -919,9 +993,8 @@ Pattern.prototype.bootstrap = () => {
// this is wrapped around mini patterns to offset krill parser location into the global js code space
function withLocationOffset(pat, offset) {
return pat.fmap((value) => {
value = typeof value === 'object' && !Array.isArray(value) ? value : { value };
let locations = (value.locations || []);
return pat._withContext((context) => {
let locations = (context.locations || []);
locations = locations.map(({ start, end }) => {
const colOffset = start.line === 1 ? offset.start.column : 0;
return {
@ -936,7 +1009,7 @@ function withLocationOffset(pat, offset) {
column: end.column - 1 + colOffset,
},
}});
return {...value, locations }
return {...context, locations }
});
}

View File

@ -2,13 +2,14 @@ import Fraction from 'fraction.js'
import { strict as assert } from 'assert';
import {TimeSpan, Hap, Pattern, pure, stack, fastcat, slowcat, cat, sequence, polyrhythm, silence, fast, timeCat,add,sub,mul,div,saw,saw2,isaw,isaw2,sine,sine2,square,square2,tri,tri2} from "../strudel.mjs";
import {TimeSpan, Hap, State, Pattern, pure, stack, fastcat, slowcat, cat, sequence, polyrhythm, silence, fast, timeCat,add,sub,mul,div,saw,saw2,isaw,isaw2,sine,sine2,square,square2,tri,tri2} from "../strudel.mjs";
//import { Time } from 'tone';
import pkg from 'tone';
const { Time } = pkg;
const st = (begin, end) => new State(ts(begin, end))
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
const hap = (whole, part, value) => new Hap(whole, part, value)
const hap = (whole, part, value, context={}) => new Hap(whole, part, value, context)
const third = Fraction(1,3)
const twothirds = Fraction(2,3)
@ -62,110 +63,110 @@ describe('Hap', function() {
describe('Pattern', function() {
describe('pure', function () {
it('Can make a pattern', function() {
assert.equal(pure("hello").query(new TimeSpan(Fraction(0.5), Fraction(2.5))).length, 3)
assert.equal(pure("hello").query(st(0.5, 2.5)).length, 3)
})
})
describe('fmap()', function () {
it('Can add things', function () {
assert.equal(pure(3).fmap(x => x + 4).firstCycle[0].value, 7)
assert.equal(pure(3).fmap(x => x + 4).firstCycle()[0].value, 7)
})
})
describe('add()', function () {
it('Can add things', function() {
assert.equal(pure(3).add(pure(4)).query(new TimeSpan(Fraction(0), Fraction(1)))[0].value, 7)
assert.equal(pure(3).add(pure(4)).query(st(0,1))[0].value, 7)
})
})
describe('sub()', function () {
it('Can subtract things', function() {
assert.equal(pure(3).sub(pure(4)).query(new TimeSpan(Fraction(0), Fraction(1)))[0].value, -1)
assert.equal(pure(3).sub(pure(4)).query(st(0,1))[0].value, -1)
})
})
describe('mul()', function () {
it('Can multiply things', function() {
assert.equal(pure(3).mul(pure(2)).firstCycle[0].value, 6)
assert.equal(pure(3).mul(pure(2)).firstCycle()[0].value, 6)
})
})
describe('div()', function () {
it('Can divide things', function() {
assert.equal(pure(3).div(pure(2)).firstCycle[0].value, 1.5)
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5)
})
})
describe('union()', function () {
it('Can union things', function () {
assert.deepStrictEqual(pure({a: 4, b: 6}).union(pure({c: 7})).firstCycle[0].value, {a: 4, b: 6, c: 7})
assert.deepStrictEqual(pure({a: 4, b: 6}).union(pure({c: 7})).firstCycle()[0].value, {a: 4, b: 6, c: 7})
})
})
describe('stack()', function () {
it('Can stack things', function () {
assert.deepStrictEqual(stack(pure("a"), pure("b"), pure("c")).firstCycle.map(h => h.value), ["a", "b", "c"])
assert.deepStrictEqual(stack(pure("a"), pure("b"), pure("c")).firstCycle().map(h => h.value), ["a", "b", "c"])
})
})
describe('_fast()', function () {
it('Makes things faster', function () {
assert.equal(pure("a")._fast(2).firstCycle.length, 2)
assert.equal(pure("a")._fast(2).firstCycle().length, 2)
})
})
describe('_fastGap()', function () {
it('Makes things faster, with a gap', function () {
assert.deepStrictEqual(
sequence("a", "b", "c")._fastGap(2).firstCycle,
sequence(["a","b","c"], silence).firstCycle
sequence("a", "b", "c")._fastGap(2).firstCycle(),
sequence(["a","b","c"], silence).firstCycle()
)
assert.deepStrictEqual(
sequence("a", "b", "c")._fastGap(3).firstCycle,
sequence(["a","b","c"], silence, silence).firstCycle
sequence("a", "b", "c")._fastGap(3).firstCycle(),
sequence(["a","b","c"], silence, silence).firstCycle()
)
})
it('Makes things faster, with a gap, when speeded up further', function () {
assert.deepStrictEqual(
sequence("a", "b", "c")._fastGap(2).fast(2).firstCycle,
sequence(["a","b","c"], silence, ["a","b","c"], silence).firstCycle
sequence("a", "b", "c")._fastGap(2).fast(2).firstCycle(),
sequence(["a","b","c"], silence, ["a","b","c"], silence).firstCycle()
)
})
})
describe('_compressSpan()', function () {
it('Can squash cycles of a pattern into a given timespan', function () {
assert.deepStrictEqual(
pure("a")._compressSpan(new TimeSpan(0.25, 0.5)).firstCycle,
sequence(silence, "a", silence, silence).firstCycle
pure("a")._compressSpan(new TimeSpan(0.25, 0.5)).firstCycle(),
sequence(silence, "a", silence, silence).firstCycle()
)
})
})
describe('fast()', function () {
it('Makes things faster', function () {
assert.equal(pure("a").fast(2).firstCycle.length, 2)
assert.equal(pure("a").fast(2).firstCycle().length, 2)
})
it('Makes things faster, with a pattern of factors', function () {
assert.equal(pure("a").fast(sequence(1,4)).firstCycle.length, 3)
assert.equal(pure("a").fast(sequence(1,4)).firstCycle().length, 3)
// .fast(sequence(1,silence) is a quick hack to cut an event in two..
assert.deepStrictEqual(pure("a").fast(sequence(1,4)).firstCycle, stack(pure("a").fast(sequence(1,silence)), sequence(silence, ["a","a"])).firstCycle)
assert.deepStrictEqual(pure("a").fast(sequence(1,4)).firstCycle(), stack(pure("a").fast(sequence(1,silence)), sequence(silence, ["a","a"])).firstCycle())
})
it('defaults to accepting sequences', function () {
assert.deepStrictEqual(
sequence(1,2,3).fast(sequence(1.5,2)).firstCycle,
sequence(1,2,3).fast(1.5,2).firstCycle
sequence(1,2,3).fast(sequence(1.5,2)).firstCycle(),
sequence(1,2,3).fast(1.5,2).firstCycle()
)
})
it("works as a static function", function () {
assert.deepStrictEqual(
sequence(1,2,3).fast(1,2).firstCycle,
fast(sequence(1,2), sequence(1,2,3)).firstCycle
sequence(1,2,3).fast(1,2).firstCycle(),
fast(sequence(1,2), sequence(1,2,3)).firstCycle()
)
})
it("works as a curried static function", function () {
assert.deepStrictEqual(
sequence(1,2,3).fast(1,2).firstCycle,
fast(sequence(1,2))(sequence(1,2,3)).firstCycle
sequence(1,2,3).fast(1,2).firstCycle(),
fast(sequence(1,2))(sequence(1,2,3)).firstCycle()
)
})
})
describe('_slow()', function () {
it('Makes things slower', function () {
assert.deepStrictEqual(pure("a")._slow(2).firstCycle[0], new Hap(new TimeSpan(Fraction(0),Fraction(2)), new TimeSpan(Fraction(0), Fraction(1)), "a"))
assert.deepStrictEqual(pure("a")._slow(2).firstCycle()[0], new Hap(new TimeSpan(Fraction(0),Fraction(2)), new TimeSpan(Fraction(0), Fraction(1)), "a"))
const pat = sequence(pure('c3'), pure('eb3')._slow(2)); // => try mini('c3 eb3/2') in repl
assert.deepStrictEqual(
pat.query(ts(0,1))[1],
pat.query(st(0,1))[1],
hap(ts(0.5,1.5), ts(1/2,1), "eb3")
)
// the following test fails
@ -181,92 +182,92 @@ describe('Pattern', function() {
})
describe('_filterValues()', function () {
it('Filters true', function () {
assert.equal(pure(true)._filterValues(x => x).firstCycle.length, 1)
assert.equal(pure(true)._filterValues(x => x).firstCycle().length, 1)
})
})
describe('when()', function () {
it('Always faster', function () {
assert.equal(pure("a").when(pure(true), x => x._fast(2)).firstCycle.length, 2)
assert.equal(pure("a").when(pure(true), x => x._fast(2)).firstCycle().length, 2)
})
it('Never faster', function () {
assert.equal(pure("a").when(pure(false), x => x._fast(2)).firstCycle.length, 1)
assert.equal(pure("a").when(pure(false), x => x._fast(2)).firstCycle().length, 1)
})
it('Can alternate', function () {
assert.deepStrictEqual(
pure(10).when(slowcat(true,false),add(3)).fast(4)._sortEventsByPart().firstCycle,
fastcat(13,10,13,10).firstCycle
pure(10).when(slowcat(true,false),add(3)).fast(4)._sortEventsByPart().firstCycle(),
fastcat(13,10,13,10).firstCycle()
)
})
})
describe('fastcat()', function () {
it('Can concatenate two things', function () {
assert.deepStrictEqual(fastcat(pure("a"), pure("b")).firstCycle.map(x => x.value), ["a", "b"])
assert.deepStrictEqual(fastcat(pure("a"), pure("b")).firstCycle().map(x => x.value), ["a", "b"])
})
})
describe('slowcat()', function () {
it('Can concatenate things slowly', function () {
assert.deepStrictEqual(slowcat("a", "b").firstCycle.map(x => x.value), ["a"])
assert.deepStrictEqual(slowcat("a", "b")._early(1).firstCycle.map(x => x.value), ["b"])
assert.deepStrictEqual(slowcat("a", slowcat("b", "c"))._early(1).firstCycle.map(x => x.value), ["b"])
assert.deepStrictEqual(slowcat("a", slowcat("b", "c"))._early(3).firstCycle.map(x => x.value), ["c"])
assert.deepStrictEqual(slowcat("a", "b").firstCycle().map(x => x.value), ["a"])
assert.deepStrictEqual(slowcat("a", "b")._early(1).firstCycle().map(x => x.value), ["b"])
assert.deepStrictEqual(slowcat("a", slowcat("b", "c"))._early(1).firstCycle().map(x => x.value), ["b"])
assert.deepStrictEqual(slowcat("a", slowcat("b", "c"))._early(3).firstCycle().map(x => x.value), ["c"])
})
})
describe('rev()', function () {
it('Can reverse things', function () {
assert.deepStrictEqual(fastcat("a","b","c").rev().firstCycle.sort((a,b) => a.part.begin.sub(b.part.begin)).map(a => a.value), ["c", "b","a"])
assert.deepStrictEqual(fastcat("a","b","c").rev().firstCycle().sort((a,b) => a.part.begin.sub(b.part.begin)).map(a => a.value), ["c", "b","a"])
})
})
describe('sequence()', () => {
it('Can work like fastcat', () => {
assert.deepStrictEqual(sequence(1,2,3).firstCycle, fastcat(1,2,3).firstCycle)
assert.deepStrictEqual(sequence(1,2,3).firstCycle(), fastcat(1,2,3).firstCycle())
})
})
describe('polyrhythm()', () => {
it('Can layer up cycles', () => {
assert.deepStrictEqual(
polyrhythm(["a","b"],["c"]).firstCycle,
stack(fastcat(pure("a"),pure("b")),pure("c")).firstCycle
polyrhythm(["a","b"],["c"]).firstCycle(),
stack(fastcat(pure("a"),pure("b")),pure("c")).firstCycle()
)
})
})
describe('every()', () => {
it('Can apply a function every 3rd time', () => {
assert.deepStrictEqual(
pure("a").every(3, x => x._fast(2))._fast(3).firstCycle,
sequence(sequence("a", "a"), "a", "a").firstCycle
pure("a").every(3, x => x._fast(2))._fast(3).firstCycle(),
sequence(sequence("a", "a"), "a", "a").firstCycle()
)
})
it("works with currying", () => {
assert.deepStrictEqual(
pure("a").every(3, fast(2))._fast(3).firstCycle,
sequence(sequence("a", "a"), "a", "a").firstCycle
pure("a").every(3, fast(2))._fast(3).firstCycle(),
sequence(sequence("a", "a"), "a", "a").firstCycle()
)
assert.deepStrictEqual(
sequence(3,4,5).every(3, add(3)).fast(5).firstCycle,
sequence(6,7,8,3,4,5,3,4,5,6,7,8,3,4,5).firstCycle
sequence(3,4,5).every(3, add(3)).fast(5).firstCycle(),
sequence(6,7,8,3,4,5,3,4,5,6,7,8,3,4,5).firstCycle()
)
assert.deepStrictEqual(
sequence(3,4,5).every(2, sub(1)).fast(5).firstCycle,
sequence(2,3,4,3,4,5,2,3,4,3,4,5,2,3,4).firstCycle
sequence(3,4,5).every(2, sub(1)).fast(5).firstCycle(),
sequence(2,3,4,3,4,5,2,3,4,3,4,5,2,3,4).firstCycle()
)
assert.deepStrictEqual(
sequence(3,4,5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle,
sequence(5,6,7,3,4,5).firstCycle
sequence(3,4,5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle(),
sequence(5,6,7,3,4,5).firstCycle()
)
})
})
describe('timeCat()', function() {
it('Can concatenate patterns with different relative durations', function() {
assert.deepStrictEqual(
sequence("a", ["a", "a"]).firstCycle,
timeCat([1,"a"], [0.5, "a"], [0.5, "a"]).firstCycle
sequence("a", ["a", "a"]).firstCycle(),
timeCat([1,"a"], [0.5, "a"], [0.5, "a"]).firstCycle()
)
})
})
describe('struct()', function() {
it('Can restructure a pattern', function() {
assert.deepStrictEqual(
sequence("a", "b").struct(sequence(true, true, true)).firstCycle,
sequence("a", "b").struct(sequence(true, true, true)).firstCycle(),
[hap(ts(0,third), ts(0,third), "a"),
hap(ts(third, twothirds), ts(third, 0.5), "a"),
hap(ts(third, twothirds), ts(0.5, twothirds), "b"),
@ -274,23 +275,23 @@ describe('Pattern', function() {
]
)
assert.deepStrictEqual(
pure("a").struct(sequence(true, [true,false], true)).firstCycle,
sequence("a", ["a", silence], "a").firstCycle,
pure("a").struct(sequence(true, [true,false], true)).firstCycle(),
sequence("a", ["a", silence], "a").firstCycle(),
)
assert.deepStrictEqual(
pure("a").struct(sequence(true, [true,false], true).invert()).firstCycle,
sequence(silence, [silence, "a"], silence).firstCycle,
pure("a").struct(sequence(true, [true,false], true).invert()).firstCycle(),
sequence(silence, [silence, "a"], silence).firstCycle(),
)
assert.deepStrictEqual(
pure("a").struct(sequence(true, [true,silence], true)).firstCycle,
sequence("a", ["a", silence], "a").firstCycle,
pure("a").struct(sequence(true, [true,silence], true)).firstCycle(),
sequence("a", ["a", silence], "a").firstCycle(),
)
})
})
describe('mask()', function() {
it('Can fragment a pattern', function() {
assert.deepStrictEqual(
sequence("a", "b").mask(sequence(true, true, true)).firstCycle,
sequence("a", "b").mask(sequence(true, true, true)).firstCycle(),
[hap(ts(0, 0.5), ts(0,third), "a"),
hap(ts(0, 0.5), ts(third, 0.5), "a"),
hap(ts(0.5, 1), ts(0.5, twothirds), "b"),
@ -300,11 +301,11 @@ describe('Pattern', function() {
})
it('Can mask off parts of a pattern', function() {
assert.deepStrictEqual(
sequence(["a", "b"], "c").mask(sequence(true, false)).firstCycle,
sequence(["a","b"], silence).firstCycle
sequence(["a", "b"], "c").mask(sequence(true, false)).firstCycle(),
sequence(["a","b"], silence).firstCycle()
)
assert.deepStrictEqual(
sequence("a").mask(sequence(true, false)).firstCycle,
sequence("a").mask(sequence(true, false)).firstCycle(),
[hap(ts(0,1),ts(0,0.5), "a")]
)
})
@ -312,69 +313,95 @@ describe('Pattern', function() {
describe('invert()', function() {
it('Can invert a binary pattern', function() {
assert.deepStrictEqual(
sequence(true, false, [true, false]).invert().firstCycle,
sequence(false, true, [false, true]).firstCycle
sequence(true, false, [true, false]).invert().firstCycle(),
sequence(false, true, [false, true]).firstCycle()
)
})
})
describe('signal()', function() {
it('Can make saw/saw2', function() {
assert.deepStrictEqual(
saw.struct(true,true,true,true).firstCycle,
sequence(1/8,3/8,5/8,7/8).firstCycle
saw.struct(true,true,true,true).firstCycle(),
sequence(1/8,3/8,5/8,7/8).firstCycle()
)
assert.deepStrictEqual(
saw2.struct(true,true,true,true).firstCycle,
sequence(-3/4,-1/4,1/4,3/4).firstCycle
saw2.struct(true,true,true,true).firstCycle(),
sequence(-3/4,-1/4,1/4,3/4).firstCycle()
)
})
it('Can make isaw/isaw2', function() {
assert.deepStrictEqual(
isaw.struct(true,true,true,true).firstCycle,
sequence(7/8,5/8,3/8,1/8).firstCycle
isaw.struct(true,true,true,true).firstCycle(),
sequence(7/8,5/8,3/8,1/8).firstCycle()
)
assert.deepStrictEqual(
isaw2.struct(true,true,true,true).firstCycle,
sequence(3/4,1/4,-1/4,-3/4).firstCycle
isaw2.struct(true,true,true,true).firstCycle(),
sequence(3/4,1/4,-1/4,-3/4).firstCycle()
)
})
})
describe('_setContext()', () => {
it('Can set the event context', () => {
assert.deepStrictEqual(
pure("a")._setContext([[[0,1],[1,2]]]).firstCycle(true),
[hap(ts(0,1),
ts(0,1),
"a",
[[[0,1],[1,2]]]
)
]
)
})
})
describe('_withContext()', () => {
it('Can update the event context', () => {
assert.deepStrictEqual(
pure("a")._setContext([[[0,1],[1,2]]])._withContext(c => [...c,[[3,4],[3,4]]]).firstCycle(true),
[hap(ts(0,1),
ts(0,1),
"a",
[[[0,1],[1,2]],[[3,4],[3,4]]]
)
]
)
})
})
describe("apply", () => {
it('Can apply a function', () => {
assert.deepStrictEqual(
sequence("a", "b")._apply(fast(2)).firstCycle,
sequence("a", "b").fast(2).firstCycle
sequence("a", "b")._apply(fast(2)).firstCycle(),
sequence("a", "b").fast(2).firstCycle()
)
}),
it('Can apply a pattern of functions', () => {
assert.deepStrictEqual(
sequence("a", "b").apply(fast(2)).firstCycle,
sequence("a", "b").fast(2).firstCycle
sequence("a", "b").apply(fast(2)).firstCycle(),
sequence("a", "b").fast(2).firstCycle()
)
assert.deepStrictEqual(
sequence("a", "b").apply(fast(2),fast(3)).firstCycle,
sequence("a", "b").fast(2,3).firstCycle
sequence("a", "b").apply(fast(2),fast(3)).firstCycle(),
sequence("a", "b").fast(2,3).firstCycle()
)
})
})
describe("layer", () => {
it('Can layer up multiple functions', () => {
assert.deepStrictEqual(
sequence(1,2,3).layer(fast(2), pat => pat.add(3,4)).firstCycle,
stack(sequence(1,2,3).fast(2), sequence(1,2,3).add(3,4)).firstCycle
sequence(1,2,3).layer(fast(2), pat => pat.add(3,4)).firstCycle(),
stack(sequence(1,2,3).fast(2), sequence(1,2,3).add(3,4)).firstCycle()
)
})
})
describe("early", () => {
it("Can shift an event earlier", () => {
assert.deepStrictEqual(
pure(30)._late(0.25).query(ts(1,2)),
pure(30)._late(0.25).query(st(1,2)),
[hap(ts(1/4,5/4), ts(1,5/4), 30), hap(ts(5/4,9/4), ts(5/4,2), 30)]
)
})
it("Can shift an event earlier, into negative time", () => {
assert.deepStrictEqual(
pure(30)._late(0.25).query(ts(0,1)),
pure(30)._late(0.25).query(st(0,1)),
[hap(ts(-3/4,1/4), ts(0,1/4), 30), hap(ts(1/4,5/4), ts(1/4,1), 30)]
)
})
@ -382,16 +409,16 @@ describe('Pattern', function() {
describe("off", () => {
it("Can offset a transformed pattern from the original", () => {
assert.deepStrictEqual(
pure(30).off(0.25, add(2)).firstCycle,
stack(pure(30), pure(30).late(0.25).add(2)).firstCycle
pure(30).off(0.25, add(2)).firstCycle(),
stack(pure(30), pure(30).late(0.25).add(2)).firstCycle()
)
})
})
describe("jux", () => {
it("Can juxtapose", () => {
assert.deepStrictEqual(
pure({a: 1}).jux(fast(2))._sortEventsByPart().firstCycle,
stack(pure({a:1, pan: 0}), pure({a:1, pan: 1}).fast(2))._sortEventsByPart().firstCycle
pure({a: 1}).jux(fast(2))._sortEventsByPart().firstCycle(),
stack(pure({a:1, pan: 0}), pure({a:1, pan: 1}).fast(2))._sortEventsByPart().firstCycle()
)
})
})