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": { "dependencies": {
"@tonaljs/tonal": "^4.6.5", "@tonaljs/tonal": "^4.6.5",
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1", "chord-voicings": "^0.0.1",
"codemirror": "^5.65.1", "codemirror": "^5.65.1",
"estraverse": "^5.3.0", "estraverse": "^5.3.0",
@ -3023,6 +3024,23 @@
"@tonaljs/time-signature": "^4.6.2" "@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": { "node_modules/@tootallnate/once": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@ -14482,6 +14500,21 @@
"@tonaljs/time-signature": "^4.6.2" "@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": { "@tootallnate/once": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",

View File

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

View File

@ -18,7 +18,7 @@ try {
console.warn('failed to decode', err); console.warn('failed to decode', err);
} }
// "balanced" | "interactive" | "playback"; // "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()); const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.getDestination());
defaultSynth.set({ defaultSynth.set({
oscillator: { type: 'triangle' }, oscillator: { type: 'triangle' },
@ -37,11 +37,12 @@ const randomTune = getRandomTune();
function App() { function App() {
const [editor, setEditor] = useState<any>(); const [editor, setEditor] = useState<any>();
const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog } = useRepl({ const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog, pending } =
tune: decoded || randomTune, useRepl({
defaultSynth, tune: decoded || randomTune,
onDraw: useCallback(markEvent(editor), [editor]), defaultSynth,
}); onDraw: useCallback(markEvent(editor), [editor]),
});
const logBox = useRef<any>(); const logBox = useRef<any>();
// scroll log box to bottom when log changes // scroll log box to bottom when log changes
useLayoutEffect(() => { useLayoutEffect(() => {
@ -51,12 +52,11 @@ function App() {
// set active pattern on ctrl+enter // set active pattern on ctrl+enter
useLayoutEffect(() => { useLayoutEffect(() => {
// TODO: make sure this is only fired when editor has focus // 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) { if (e.ctrlKey || e.altKey) {
switch (e.code) { switch (e.code) {
case 'Enter': case 'Enter':
activateCode(); await activateCode();
!cycle.started && cycle.start();
break; break;
case 'Period': case 'Period':
cycle.stop(); cycle.stop();
@ -88,11 +88,11 @@ function App() {
</div> </div>
<div className="flex space-x-4"> <div className="flex space-x-4">
<button <button
onClick={() => { onClick={async () => {
const _code = getRandomTune(); const _code = getRandomTune();
console.log('tune', _code); // uncomment this to debug when random code fails console.log('tune', _code); // uncomment this to debug when random code fails
setCode(_code); setCode(_code);
const parsed = evaluate(_code); const parsed = await evaluate(_code);
// Tone.Transport.cancel(Tone.Transport.seconds); // Tone.Transport.cancel(Tone.Transport.seconds);
setPattern(parsed.pattern); 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" className="flex-none w-full border border-gray-700 p-2 bg-slate-700 hover:bg-slate-500"
onClick={() => togglePlay()} onClick={() => togglePlay()}
> >
{cycle.started ? 'pause' : 'play'} {!pending ? <>{cycle.started ? 'pause' : 'play'}</> : <>loading...</>}
</button> </button>
<textarea <textarea
className="grow bg-[#283237] border-0 text-xs min-h-[200px]" 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) => { export const markEvent = (editor) => (time, event) => {
const locs = event.value.locations; const locs = event.context.locations;
if (!locs || !editor) { if (!locs || !editor) {
return; 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 // this will add everything to global scope, which is accessed by eval
Object.assign(globalThis, bootstrapped, Tone, toneHelpers); 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 const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
// console.log('shapeshifted', shapeshifted); // console.log('shapeshifted', shapeshifted);
let evaluated = eval(shapeshifted); let evaluated = await eval(shapeshifted);
if (typeof evaluated === 'function') { if (typeof evaluated === 'function') {
evaluated = evaluated(); evaluated = evaluated();
} }

View File

@ -101,10 +101,11 @@ export function patternifyAST(ast: any): any {
return ast.source_; return ast.source_;
} }
const { start, end } = ast.location_; const { start, end } = ast.location_;
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
// return ast.source_; // return ast.source_;
// the following line expects the shapeshifter to wrap this in withLocationOffset // 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 // 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_); return patternifyAST(ast.source_);
case 'stretch': case 'stretch':

View File

@ -92,15 +92,16 @@ export default (code) => {
// add to location to pure(x) calls // add to location to pure(x) calls
if (node.type === 'CallExpression' && node.callee.name === 'pure') { if (node.type === 'CallExpression' && node.callee.name === 'pure') {
const literal = node.arguments[0]; const literal = node.arguments[0];
const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]]; // const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
return reifyWithLocation(value + '', node.arguments[0], ast.locations, artificialNodes); // console.log('value',value);
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
} }
// replace pseudo note variables // replace pseudo note variables
if (node.type === 'IdentifierExpression') { if (node.type === 'IdentifierExpression') {
if (isNote(node.name)) { if (isNote(node.name)) {
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name; const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
if (addLocations && isMarkable) { if (addLocations && isMarkable) {
return reifyWithLocation(value, node, ast.locations, artificialNodes); return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes);
} }
return new LiteralStringExpression({ value }); return new LiteralStringExpression({ value });
} }
@ -110,7 +111,7 @@ export default (code) => {
} }
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) { if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) {
// console.log('add', node); // console.log('add', node);
return reifyWithLocation(node.value, node, ast.locations, artificialNodes); return reifyWithLocation(node, node, ast.locations, artificialNodes);
} }
if (!addMiniLocations) { if (!addMiniLocations) {
return wrapFunction('reify', node); 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 // 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 // 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({ const withLocation = new CallExpression({
callee: new StaticMemberExpression({ callee: new StaticMemberExpression({
object: wrapFunction('reify', new LiteralStringExpression({ value })), object: wrapFunction('reify', literalNode),
property: 'withLocation', property: 'withLocation',
}), }),
arguments: [getLocationObject(node, locations)], arguments: [getLocationObject(node, locations)],

View File

@ -3,21 +3,6 @@ import { Pattern as _Pattern } from '../../strudel.mjs';
const Pattern = _Pattern as any; 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 // modulo that works with negative numbers e.g. mod(-1, 3) = 2
const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m); const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
@ -60,24 +45,16 @@ function scaleTranspose(scale: string, offset: number, note: string) {
return n + o; 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) { Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
return this._mapNotes(({ value, scale }: NoteEvent) => { return this._withEvent((event) => {
const interval = !isNaN(Number(intervalOrSemitones)) const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones as number) ? Interval.fromSemitones(intervalOrSemitones as number)
: String(intervalOrSemitones); : String(intervalOrSemitones);
if (typeof value === 'number') { if (typeof event.value === 'number') {
const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval; 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 // or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or
Pattern.prototype._scaleTranspose = function (offset: number | string) { Pattern.prototype._scaleTranspose = function (offset: number | string) {
return this._mapNotes(({ value, scale }: NoteEvent) => { return this._withEvent((event) => {
if (!scale) { if (!event.context.scale) {
throw new Error('can only use scaleTranspose after .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'); 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) { Pattern.prototype._scale = function (scale: string) {
return this._mapNotes((value) => { return this._withEvent((event) => {
let note = value.value; let note = event.value;
const asNumber = Number(note); const asNumber = Number(note);
if (!isNaN(asNumber)) { if (!isNaN(asNumber)) {
let [tonic, scaleName] = Scale.tokenize(scale); let [tonic, scaleName] = Scale.tokenize(scale);
const { pc, oct = 3 } = Note.get(tonic); const { pc, oct = 3 } = Note.get(tonic);
note = scaleTranspose(pc + ' ' + scaleName, asNumber, pc + oct); 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, Sampler,
getDestination getDestination
} from 'tone'; } from 'tone';
import { Piano } from '@tonejs/piano';
// what about // what about
// https://www.charlie-roberts.com/gibberish/playground/ // 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 // with this function, you can play the pattern with any tone synth
Pattern.prototype.tone = function (instrument) { Pattern.prototype.tone = function (instrument) {
// instrument.toDestination(); // instrument.toDestination();
return this.fmap((value: any) => { return this._withEvent((event) => {
value = typeof value !== 'object' && !Array.isArray(value) ? { value } : value;
const onTrigger = (time, event) => { const onTrigger = (time, event) => {
if (instrument.constructor.name === 'PluckSynth') { if (instrument.constructor.name === 'PluckSynth') {
instrument.triggerAttack(value.value, time); instrument.triggerAttack(event.value, time);
} else if (instrument.constructor.name === 'NoiseSynth') { } else if (instrument.constructor.name === 'NoiseSynth') {
instrument.triggerAttackRelease(event.duration, time); // noise has no value 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 { } 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 polysynth = (options) => new PolySynth(options);
export const sampler = (options) => new Sampler(options); export const sampler = (options) => new Sampler(options);
export const synth = (options) => new Synth(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 // effect helpers
export const vol = (v) => new Gain(v); 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 = new PolySynth(Synth, instrumentConfig).toDestination();
this.instrument = poly(type); this.instrument = poly(type);
} }
return this.fmap((value: any) => { return this._withEvent((event: any) => {
value = typeof value !== 'object' && !Array.isArray(value) ? { value } : value;
const onTrigger = (time, event) => { const onTrigger = (time, event) => {
this.instrument.set(instrumentConfig); 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') { Pattern.prototype._synth = function (type: any = 'triangle') {
return this.fmap((value: any) => { return this._withEvent((event: any) => {
value = typeof value !== 'object' && !Array.isArray(value) ? { value } : value;
const instrumentConfig: any = { const instrumentConfig: any = {
oscillator: { type }, oscillator: { type },
envelope: { attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01 }, 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); instrument.set(instrumentConfig);
return instrument; return instrument;
}; };
const onTrigger = getTrigger(() => getInstrument().toDestination(), value.value); const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
return { ...value, getInstrument, instrumentConfig, onTrigger }; return event.setContext({ ...event.context, getInstrument, instrumentConfig, onTrigger });
}); });
}; };
Pattern.prototype.adsr = function (attack = 0.01, decay = 0.01, sustain = 0.6, release = 0.01) { Pattern.prototype.adsr = function (attack = 0.01, decay = 0.01, sustain = 0.6, release = 0.01) {
return this.fmap((value: any) => { return this._withEvent((event: any) => {
if (!value?.getInstrument) { if (!event.context.getInstrument) {
throw new Error('cannot chain adsr: need instrument first (like synth)'); 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 getInstrument = () => {
const instrument = value.getInstrument(); const instrument = event.context.getInstrument();
instrument.set(instrumentConfig); instrument.set(instrumentConfig);
return instrument; return instrument;
}; };
const onTrigger = getTrigger(() => getInstrument().toDestination(), value.value); const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
return { ...value, getInstrument, instrumentConfig, onTrigger }; return event.setContext({ ...event.context, getInstrument, instrumentConfig, onTrigger });
}); });
}; };
Pattern.prototype.chain = function (...effectGetters: any) { Pattern.prototype.chain = function (...effectGetters: any) {
return this.fmap((value: any) => { return this._withEvent((event: any) => {
if (!value?.getInstrument) { if (!event.context?.getInstrument) {
throw new Error('cannot chain: need instrument first (like synth)'); 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 getChain = () => {
const effects = chain.map((getEffect: any) => getEffect()); 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); const onTrigger = getTrigger(getChain, event.value);
return { ...value, getChain, onTrigger, chain }; return event.setContext({ ...event.context, getChain, onTrigger, chain });
}); });
}; };

View File

@ -309,7 +309,6 @@ export const loungerave = `() => {
//.early("0.25 0"); //.early("0.25 0");
}`; }`;
export const caverave = `() => { export const caverave = `() => {
const delay = new FeedbackDelay(1/8, .4).chain(vol(0.5), out()); const delay = new FeedbackDelay(1/8, .4).chain(vol(0.5), out());
const kick = new MembraneSynth().chain(vol(.8), out()); const kick = new MembraneSynth().chain(vol(.8), out());
@ -342,7 +341,6 @@ export const caverave = `() => {
).slow(2); ).slow(2);
}`; }`;
export const callcenterhero = `()=>{ export const callcenterhero = `()=>{
const bpm = 90; const bpm = 90;
const lead = polysynth().set({...osc('sine4'),...adsr(.004)}).chain(vol(0.15),out()) 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") "[2,4]/4".scale('D dorian').apply(t).tone(instr('pad')).mask("<x x x ~>/8")
).fast(6/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; whole: TimeSpan;
part: TimeSpan; part: TimeSpan;
value: T; value: T;
context: any;
show: () => string; show: () => string;
} }
export declare interface Pattern<T = any> { 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 type { ToneEventCallback } from 'tone';
import * as Tone from 'tone'; import * as Tone from 'tone';
import { TimeSpan } from '../../strudel.mjs'; import { TimeSpan, State } from '../../strudel.mjs';
import type { Hap } from './types'; import type { Hap } from './types';
export declare interface UseCycleProps { export declare interface UseCycleProps {
onEvent: ToneEventCallback<any>; onEvent: ToneEventCallback<any>;
onQuery?: (query: TimeSpan) => Hap[]; onQuery?: (state: State) => Hap[];
onSchedule?: (events: Hap[], cycle: number) => void; onSchedule?: (events: Hap[], cycle: number) => void;
onDraw?: ToneEventCallback<any>; onDraw?: ToneEventCallback<any>;
ready?: boolean; // if false, query will not be called on change props 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 // pull events with onQuery + count up to next cycle
const query = (cycle = activeCycle()) => { const query = (cycle = activeCycle()) => {
const timespan = new TimeSpan(cycle, cycle + 1); const timespan = new TimeSpan(cycle, cycle + 1);
const events = onQuery?.(timespan) || []; const events = onQuery?.(new State(timespan)) || [];
onSchedule?.(events, cycle); onSchedule?.(events, cycle);
// cancel events after current query. makes sure no old events are player for rescheduled cycles // cancel events after current query. makes sure no old events are player for rescheduled cycles
// console.log('schedule', cycle); // console.log('schedule', cycle);
@ -47,6 +47,7 @@ function useCycle(props: UseCycleProps) {
time: event.part.begin.valueOf(), time: event.part.begin.valueOf(),
duration: event.whole.end.sub(event.whole.begin).valueOf(), duration: event.whole.end.sub(event.whole.begin).valueOf(),
value: event.value, value: event.value,
context: event.context,
}; };
onEvent(time, toneEvent); onEvent(time, toneEvent);
Tone.Draw.schedule(() => { Tone.Draw.schedule(() => {

View File

@ -17,19 +17,22 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
const [activeCode, setActiveCode] = useState<string>(); const [activeCode, setActiveCode] = useState<string>();
const [log, setLog] = useState(''); const [log, setLog] = useState('');
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
const [pending, setPending] = useState(false);
const [hash, setHash] = useState(''); const [hash, setHash] = useState('');
const [pattern, setPattern] = useState<Pattern>(); const [pattern, setPattern] = useState<Pattern>();
const dirty = code !== activeCode || error; const dirty = code !== activeCode || error;
const generateHash = () => encodeURIComponent(btoa(code)); const generateHash = () => encodeURIComponent(btoa(code));
const activateCode = (_code = code) => { const activateCode = async (_code = code) => {
!cycle.started && cycle.start();
broadcast({ type: 'start', from: id });
if (activeCode && !dirty) { if (activeCode && !dirty) {
setError(undefined); setError(undefined);
!cycle.started && cycle.start();
return; return;
} }
try { try {
const parsed = evaluate(_code); setPending(true);
const parsed = await evaluate(_code);
!cycle.started && cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern); setPattern(() => parsed.pattern);
if (autolink) { if (autolink) {
window.location.hash = '#' + encodeURIComponent(btoa(code)); window.location.hash = '#' + encodeURIComponent(btoa(code));
@ -37,6 +40,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
setHash(generateHash()); setHash(generateHash());
setError(undefined); setError(undefined);
setActiveCode(_code); setActiveCode(_code);
setPending(false);
} catch (err: any) { } catch (err: any) {
err.message = 'evaluation error: ' + err.message; err.message = 'evaluation error: ' + err.message;
console.warn(err); console.warn(err);
@ -47,7 +51,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
// logs events of cycle // logs events of cycle
const logCycle = (_events: any, cycle: any) => { const logCycle = (_events: any, cycle: any) => {
if (_events.length) { 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 // cycle hook to control scheduling
@ -57,8 +61,9 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
(time, event) => { (time, event) => {
try { try {
onEvent?.(event); onEvent?.(event);
if (!event.value?.onTrigger) { const { onTrigger } = event.context;
const note = event.value?.value || event.value; if (!onTrigger) {
const note = event.value;
if (!isNote(note)) { if (!isNote(note)) {
throw new Error('not a note: ' + 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); /* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */ throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else { } else {
const { onTrigger } = event.value;
onTrigger(time, event); onTrigger(time, event);
} }
} catch (err: any) { } catch (err: any) {
@ -82,9 +86,9 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
[onEvent] [onEvent]
), ),
onQuery: useCallback( onQuery: useCallback(
(span) => { (state) => {
try { try {
return pattern?.query(span) || []; return pattern?.query(state) || [];
} catch (err: any) { } catch (err: any) {
console.warn(err); console.warn(err);
err.message = 'query error: ' + err.message; err.message = 'query error: ' + err.message;
@ -146,6 +150,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
}; };
return { return {
pending,
code, code,
setCode, setCode,
pattern, pattern,

View File

@ -19,7 +19,7 @@ Pattern.prototype.fmapNested = function (func) {
.map((event) => .map((event) =>
reify(func(event)) reify(func(event))
.query(span) .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() .flat()
); );
@ -32,17 +32,18 @@ Pattern.prototype.voicings = function (range) {
range = ['F3', 'A4']; range = ['F3', 'A4'];
} }
return this.fmapNested((event) => { return this.fmapNested((event) => {
lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range); lastVoicing = getVoicing(event.value, lastVoicing, range);
return stack(...lastVoicing); return stack(...lastVoicing)._withContext(() => ({
locations: event.context.locations || [],
}));
}); });
}; };
Pattern.prototype.rootNotes = function (octave = 2) { Pattern.prototype.rootNotes = function (octave = 2) {
// range = ['G1', 'C3'] // range = ['G1', 'C3']
return this._mapNotes((value) => { return this.fmap((value) => {
const [_, root] = value.value.match(/^([a-gA-G])[b#]?.*$/); const [_, root] = value.match(/^([a-gA-G])[b#]?.*$/);
const bassNote = root + octave; return root + octave;
return { ...value, value: bassNote };
}); });
}; };

View File

@ -172,23 +172,29 @@ class Hap {
then the whole will be returned as None, in which case the given then the whole will be returned as None, in which case the given
value will have been sampled from the point halfway between the value will have been sampled from the point halfway between the
start and end of the 'part' timespan. 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.whole = whole
this.part = part this.part = part
this.value = value this.value = value
this.context = context
this.stateful = stateful
if (stateful) {
assert(typeof this.value === "function", "Stateful values must be functions");
}
} }
withSpan(func) { withSpan(func) {
// Returns a new event with the function f applies to the event timespan. // Returns a new event with the function f applies to the event timespan.
const whole = this.whole ? func(this.whole) : undefined 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) { withValue(func) {
// Returns a new event with the function f applies to the event value. // 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() { hasOnset() {
@ -197,6 +203,15 @@ class Hap {
return (this.whole != undefined) && (this.whole.begin.equals(this.part.begin)) 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) { spanEquals(other) {
return((this.whole == undefined && other.whole == undefined) return((this.whole == undefined && other.whole == undefined)
|| this.whole.equals(other.whole) || this.whole.equals(other.whole)
@ -212,7 +227,32 @@ class Hap {
} }
show() { 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 // easier to express, as all events are then constrained to happen within
// a cycle. // a cycle.
const pat = this 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) return new Pattern(q)
} }
withQuerySpan(func) { withQuerySpan(func) {
return new Pattern(span => this.query(func(span))) return new Pattern(state => this.query(state.withSpan(func)))
} }
withQueryTime(func) { withQueryTime(func) {
// Returns a new pattern, with the function applied to both the begin // Returns a new pattern, with the function applied to both the begin
// and end of the the query timespan // 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) { withEventSpan(func) {
// Returns a new pattern, with the function applied to each event // Returns a new pattern, with the function applied to each event
// timespan. // 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) { withEventTime(func) {
@ -272,21 +314,36 @@ class Pattern {
} }
_withEvents(func) { _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) { withLocation(location) {
return this.fmap(value => { return this._withContext((context) => {
value = typeof value === 'object' && !Array.isArray(value) ? value : { value }; const locations = (context.locations || []).concat([location])
const locations = (value.locations || []).concat([location]); return { ...context, locations }
return {...value, locations } });
})
} }
withValue(func) { withValue(func) {
// Returns a new pattern, with the function applied to the value of // Returns a new pattern, with the function applied to the value of
// each event. It has the alias 'fmap'. // 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 // alias
@ -295,11 +352,11 @@ class Pattern {
} }
_filterEvents(event_test) { _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) { _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() { _removeUndefineds() {
@ -318,20 +375,21 @@ class Pattern {
// resolve wholes, applies a given pattern of values to that // resolve wholes, applies a given pattern of values to that
// pattern of functions. // pattern of functions.
const pat_func = this const pat_func = this
const query = function(span) { const query = function(state) {
const event_funcs = pat_func.query(span) const event_funcs = pat_func.query(state)
const event_vals = pat_val.query(span) const event_vals = pat_val.query(state)
const apply = function(event_func, event_val) { const apply = function(event_func, event_val) {
const s = event_func.part.intersection(event_val.part) const s = event_func.part.intersection(event_val.part)
if (s == undefined) { if (s == undefined) {
return 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 flatten(event_funcs.map(event_func => removeUndefineds(event_vals.map(event_val => apply(event_func, event_val)))))
} }
return new Pattern(query) return new Pattern(query)
} }
appBoth(pat_val) { appBoth(pat_val) {
// Tidal's <*> // Tidal's <*>
@ -347,15 +405,19 @@ class Pattern {
appLeft(pat_val) { appLeft(pat_val) {
const pat_func = this const pat_func = this
const query = function(span) { const query = function(state) {
const haps = [] const haps = []
for (const hap_func of pat_func.query(span)) { for (const hap_func of pat_func.query(state)) {
const event_vals = pat_val.query(hap_func.part) const event_vals = pat_val.query(state.setSpan(hap_func.part))
for (const hap_val of event_vals) { for (const hap_val of event_vals) {
const new_whole = hap_func.whole const new_whole = hap_func.whole
const new_part = hap_func.part.intersection_e(hap_val.part) const new_part = hap_func.part.intersection_e(hap_val.part)
const new_value = hap_func.value(hap_val.value) 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) haps.push(hap)
} }
} }
@ -367,15 +429,19 @@ class Pattern {
appRight(pat_val) { appRight(pat_val) {
const pat_func = this const pat_func = this
const query = function(span) { const query = function(state) {
const haps = [] const haps = []
for (const hap_val of pat_val.query(span)) { for (const hap_val of pat_val.query(state)) {
const hap_funcs = pat_func.query(hap_val.part) const hap_funcs = pat_func.query(state.setSpan(hap_val.part))
for (const hap_func of hap_funcs) { for (const hap_func of hap_funcs) {
const new_whole = hap_val.whole const new_whole = hap_val.whole
const new_part = hap_func.part.intersection_e(hap_val.part) const new_part = hap_func.part.intersection_e(hap_val.part)
const new_value = hap_func.value(hap_val.value) 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) haps.push(hap)
} }
} }
@ -384,8 +450,12 @@ class Pattern {
return new Pattern(query) return new Pattern(query)
} }
get firstCycle() { firstCycle(with_context=false) {
return this.query(new TimeSpan(Fraction(0), Fraction(1))) var self = this
if (!with_context) {
self = self._stripContext()
}
return self.query(new State(new TimeSpan(Fraction(0), Fraction(1))))
} }
_sortEventsByPart() { _sortEventsByPart() {
@ -418,16 +488,18 @@ class Pattern {
_bindWhole(choose_whole, func) { _bindWhole(choose_whole, func) {
const pat_val = this const pat_val = this
const query = function(span) { const query = function(state) {
const withWhole = function(a, b) { const withWhole = function(a, b) {
return new Hap(choose_whole(a.whole, b.whole), b.part, return new Hap(choose_whole(a.whole, b.whole), b.part, b.value, {
b.value ...a.context,
) ...b.context,
locations: (a.context.locations || []).concat(b.context.locations || []),
});
} }
const match = function (a) { 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) return new Pattern(query)
} }
@ -590,7 +662,8 @@ class Pattern {
rev() { rev() {
const pat = this const pat = this
const query = function(span) { const query = function(state) {
const span = state.span
const cycle = span.begin.sam() const cycle = span.begin.sam()
const next_cycle = span.begin.nextSam() const next_cycle = span.begin.nextSam()
const reflect = function(to_reflect) { const reflect = function(to_reflect) {
@ -601,7 +674,7 @@ class Pattern {
reflected.end = tmp reflected.end = tmp
return reflected return reflected
} }
const haps = pat.query(reflect(span)) const haps = pat.query(state.setSpan(reflect(span)))
return haps.map(hap => hap.withSpan(reflect)) return haps.map(hap => hap.withSpan(reflect))
} }
return new Pattern(query)._splitQueries() return new Pattern(query)._splitQueries()
@ -679,8 +752,8 @@ const silence = new Pattern(_ => [])
function pure(value) { function pure(value) {
// A discrete value that repeats once per cycle // A discrete value that repeats once per cycle
function query(span) { function query(state) {
return span.spanCycles.map(subspan => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value)) return state.span.spanCycles.map(subspan => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value))
} }
return new Pattern(query) return new Pattern(query)
} }
@ -691,7 +764,7 @@ function steady(value) {
} }
export const signal = func => { 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) return new Pattern(query)
} }
@ -728,7 +801,7 @@ function reify(thing) {
function stack(...pats) { function stack(...pats) {
const reified = pats.map(pat => reify(pat)) 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) return new Pattern(query)
} }
@ -736,7 +809,8 @@ function slowcat(...pats) {
// Concatenation: combines a list of patterns, switching between them // Concatenation: combines a list of patterns, switching between them
// successively, one per cycle. // successively, one per cycle.
pats = pats.map(reify) 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_n = Math.floor(span.begin) % pats.length;
const pat = pats[pat_n] const pat = pats[pat_n]
if (!pat) { if (!pat) {
@ -747,7 +821,7 @@ function slowcat(...pats) {
// For example if three patterns are slowcat-ed, the fourth cycle of the result should // 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. // be the second (rather than fourth) cycle from the first pattern.
const offset = span.begin.floor().sub(span.begin.div(pats.length).floor()) 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() return new Pattern(query)._splitQueries()
} }
@ -756,10 +830,10 @@ function slowcatPrime(...pats) {
// Concatenation: combines a list of patterns, switching between them // Concatenation: combines a list of patterns, switching between them
// successively, one per cycle. Unlike slowcat, this version will skip cycles. // successively, one per cycle. Unlike slowcat, this version will skip cycles.
pats = pats.map(reify) pats = pats.map(reify)
const query = function(span) { const query = function(state) {
const pat_n = Math.floor(span.begin) % pats.length const pat_n = Math.floor(state.span.begin) % pats.length
const pat = pats[pat_n] const pat = pats[pat_n]
return pat.query(span) return pat.query(state)
} }
return new Pattern(query)._splitQueries() 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 // this is wrapped around mini patterns to offset krill parser location into the global js code space
function withLocationOffset(pat, offset) { function withLocationOffset(pat, offset) {
return pat.fmap((value) => { return pat._withContext((context) => {
value = typeof value === 'object' && !Array.isArray(value) ? value : { value }; let locations = (context.locations || []);
let locations = (value.locations || []);
locations = locations.map(({ start, end }) => { locations = locations.map(({ start, end }) => {
const colOffset = start.line === 1 ? offset.start.column : 0; const colOffset = start.line === 1 ? offset.start.column : 0;
return { return {
@ -936,7 +1009,7 @@ function withLocationOffset(pat, offset) {
column: end.column - 1 + colOffset, 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 { 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 { Time } from 'tone';
import pkg from 'tone'; import pkg from 'tone';
const { Time } = pkg; const { Time } = pkg;
const st = (begin, end) => new State(ts(begin, end))
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(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 third = Fraction(1,3)
const twothirds = Fraction(2,3) const twothirds = Fraction(2,3)
@ -62,110 +63,110 @@ describe('Hap', function() {
describe('Pattern', function() { describe('Pattern', function() {
describe('pure', function () { describe('pure', function () {
it('Can make a pattern', 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 () { describe('fmap()', function () {
it('Can add things', 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 () { describe('add()', function () {
it('Can add things', 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 () { describe('sub()', function () {
it('Can subtract things', 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 () { describe('mul()', function () {
it('Can multiply things', 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 () { describe('div()', function () {
it('Can divide things', 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 () { describe('union()', function () {
it('Can union things', 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 () { describe('stack()', function () {
it('Can stack things', 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 () { describe('_fast()', function () {
it('Makes things faster', 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 () { describe('_fastGap()', function () {
it('Makes things faster, with a gap', function () { it('Makes things faster, with a gap', function () {
assert.deepStrictEqual( assert.deepStrictEqual(
sequence("a", "b", "c")._fastGap(2).firstCycle, sequence("a", "b", "c")._fastGap(2).firstCycle(),
sequence(["a","b","c"], silence).firstCycle sequence(["a","b","c"], silence).firstCycle()
) )
assert.deepStrictEqual( assert.deepStrictEqual(
sequence("a", "b", "c")._fastGap(3).firstCycle, sequence("a", "b", "c")._fastGap(3).firstCycle(),
sequence(["a","b","c"], silence, silence).firstCycle sequence(["a","b","c"], silence, silence).firstCycle()
) )
}) })
it('Makes things faster, with a gap, when speeded up further', function () { it('Makes things faster, with a gap, when speeded up further', function () {
assert.deepStrictEqual( assert.deepStrictEqual(
sequence("a", "b", "c")._fastGap(2).fast(2).firstCycle, sequence("a", "b", "c")._fastGap(2).fast(2).firstCycle(),
sequence(["a","b","c"], silence, ["a","b","c"], silence).firstCycle sequence(["a","b","c"], silence, ["a","b","c"], silence).firstCycle()
) )
}) })
}) })
describe('_compressSpan()', function () { describe('_compressSpan()', function () {
it('Can squash cycles of a pattern into a given timespan', function () { it('Can squash cycles of a pattern into a given timespan', function () {
assert.deepStrictEqual( assert.deepStrictEqual(
pure("a")._compressSpan(new TimeSpan(0.25, 0.5)).firstCycle, pure("a")._compressSpan(new TimeSpan(0.25, 0.5)).firstCycle(),
sequence(silence, "a", silence, silence).firstCycle sequence(silence, "a", silence, silence).firstCycle()
) )
}) })
}) })
describe('fast()', function () { describe('fast()', function () {
it('Makes things faster', 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 () { 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.. // .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 () { it('defaults to accepting sequences', function () {
assert.deepStrictEqual( assert.deepStrictEqual(
sequence(1,2,3).fast(sequence(1.5,2)).firstCycle, sequence(1,2,3).fast(sequence(1.5,2)).firstCycle(),
sequence(1,2,3).fast(1.5,2).firstCycle sequence(1,2,3).fast(1.5,2).firstCycle()
) )
}) })
it("works as a static function", function () { it("works as a static function", function () {
assert.deepStrictEqual( assert.deepStrictEqual(
sequence(1,2,3).fast(1,2).firstCycle, sequence(1,2,3).fast(1,2).firstCycle(),
fast(sequence(1,2), sequence(1,2,3)).firstCycle fast(sequence(1,2), sequence(1,2,3)).firstCycle()
) )
}) })
it("works as a curried static function", function () { it("works as a curried static function", function () {
assert.deepStrictEqual( assert.deepStrictEqual(
sequence(1,2,3).fast(1,2).firstCycle, sequence(1,2,3).fast(1,2).firstCycle(),
fast(sequence(1,2))(sequence(1,2,3)).firstCycle fast(sequence(1,2))(sequence(1,2,3)).firstCycle()
) )
}) })
}) })
describe('_slow()', function () { describe('_slow()', function () {
it('Makes things slower', 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 const pat = sequence(pure('c3'), pure('eb3')._slow(2)); // => try mini('c3 eb3/2') in repl
assert.deepStrictEqual( 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") hap(ts(0.5,1.5), ts(1/2,1), "eb3")
) )
// the following test fails // the following test fails
@ -181,92 +182,92 @@ describe('Pattern', function() {
}) })
describe('_filterValues()', function () { describe('_filterValues()', function () {
it('Filters true', 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 () { describe('when()', function () {
it('Always faster', 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 () { 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 () { it('Can alternate', function () {
assert.deepStrictEqual( assert.deepStrictEqual(
pure(10).when(slowcat(true,false),add(3)).fast(4)._sortEventsByPart().firstCycle, pure(10).when(slowcat(true,false),add(3)).fast(4)._sortEventsByPart().firstCycle(),
fastcat(13,10,13,10).firstCycle fastcat(13,10,13,10).firstCycle()
) )
}) })
}) })
describe('fastcat()', function () { describe('fastcat()', function () {
it('Can concatenate two things', 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 () { describe('slowcat()', function () {
it('Can concatenate things slowly', function () { it('Can concatenate things slowly', function () {
assert.deepStrictEqual(slowcat("a", "b").firstCycle.map(x => x.value), ["a"]) 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", "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(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", slowcat("b", "c"))._early(3).firstCycle().map(x => x.value), ["c"])
}) })
}) })
describe('rev()', function () { describe('rev()', function () {
it('Can reverse things', 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()', () => { describe('sequence()', () => {
it('Can work like fastcat', () => { 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()', () => { describe('polyrhythm()', () => {
it('Can layer up cycles', () => { it('Can layer up cycles', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
polyrhythm(["a","b"],["c"]).firstCycle, polyrhythm(["a","b"],["c"]).firstCycle(),
stack(fastcat(pure("a"),pure("b")),pure("c")).firstCycle stack(fastcat(pure("a"),pure("b")),pure("c")).firstCycle()
) )
}) })
}) })
describe('every()', () => { describe('every()', () => {
it('Can apply a function every 3rd time', () => { it('Can apply a function every 3rd time', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
pure("a").every(3, x => x._fast(2))._fast(3).firstCycle, pure("a").every(3, x => x._fast(2))._fast(3).firstCycle(),
sequence(sequence("a", "a"), "a", "a").firstCycle sequence(sequence("a", "a"), "a", "a").firstCycle()
) )
}) })
it("works with currying", () => { it("works with currying", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
pure("a").every(3, fast(2))._fast(3).firstCycle, pure("a").every(3, fast(2))._fast(3).firstCycle(),
sequence(sequence("a", "a"), "a", "a").firstCycle sequence(sequence("a", "a"), "a", "a").firstCycle()
) )
assert.deepStrictEqual( assert.deepStrictEqual(
sequence(3,4,5).every(3, add(3)).fast(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 sequence(6,7,8,3,4,5,3,4,5,6,7,8,3,4,5).firstCycle()
) )
assert.deepStrictEqual( assert.deepStrictEqual(
sequence(3,4,5).every(2, sub(1)).fast(5).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 sequence(2,3,4,3,4,5,2,3,4,3,4,5,2,3,4).firstCycle()
) )
assert.deepStrictEqual( assert.deepStrictEqual(
sequence(3,4,5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle, sequence(3,4,5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle(),
sequence(5,6,7,3,4,5).firstCycle sequence(5,6,7,3,4,5).firstCycle()
) )
}) })
}) })
describe('timeCat()', function() { describe('timeCat()', function() {
it('Can concatenate patterns with different relative durations', function() { it('Can concatenate patterns with different relative durations', function() {
assert.deepStrictEqual( assert.deepStrictEqual(
sequence("a", ["a", "a"]).firstCycle, sequence("a", ["a", "a"]).firstCycle(),
timeCat([1,"a"], [0.5, "a"], [0.5, "a"]).firstCycle timeCat([1,"a"], [0.5, "a"], [0.5, "a"]).firstCycle()
) )
}) })
}) })
describe('struct()', function() { describe('struct()', function() {
it('Can restructure a pattern', function() { it('Can restructure a pattern', function() {
assert.deepStrictEqual( 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(0,third), ts(0,third), "a"),
hap(ts(third, twothirds), ts(third, 0.5), "a"), hap(ts(third, twothirds), ts(third, 0.5), "a"),
hap(ts(third, twothirds), ts(0.5, twothirds), "b"), hap(ts(third, twothirds), ts(0.5, twothirds), "b"),
@ -274,23 +275,23 @@ describe('Pattern', function() {
] ]
) )
assert.deepStrictEqual( assert.deepStrictEqual(
pure("a").struct(sequence(true, [true,false], true)).firstCycle, pure("a").struct(sequence(true, [true,false], true)).firstCycle(),
sequence("a", ["a", silence], "a").firstCycle, sequence("a", ["a", silence], "a").firstCycle(),
) )
assert.deepStrictEqual( assert.deepStrictEqual(
pure("a").struct(sequence(true, [true,false], true).invert()).firstCycle, pure("a").struct(sequence(true, [true,false], true).invert()).firstCycle(),
sequence(silence, [silence, "a"], silence).firstCycle, sequence(silence, [silence, "a"], silence).firstCycle(),
) )
assert.deepStrictEqual( assert.deepStrictEqual(
pure("a").struct(sequence(true, [true,silence], true)).firstCycle, pure("a").struct(sequence(true, [true,silence], true)).firstCycle(),
sequence("a", ["a", silence], "a").firstCycle, sequence("a", ["a", silence], "a").firstCycle(),
) )
}) })
}) })
describe('mask()', function() { describe('mask()', function() {
it('Can fragment a pattern', function() { it('Can fragment a pattern', function() {
assert.deepStrictEqual( 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(0,third), "a"),
hap(ts(0, 0.5), ts(third, 0.5), "a"), hap(ts(0, 0.5), ts(third, 0.5), "a"),
hap(ts(0.5, 1), ts(0.5, twothirds), "b"), 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() { it('Can mask off parts of a pattern', function() {
assert.deepStrictEqual( assert.deepStrictEqual(
sequence(["a", "b"], "c").mask(sequence(true, false)).firstCycle, sequence(["a", "b"], "c").mask(sequence(true, false)).firstCycle(),
sequence(["a","b"], silence).firstCycle sequence(["a","b"], silence).firstCycle()
) )
assert.deepStrictEqual( 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")] [hap(ts(0,1),ts(0,0.5), "a")]
) )
}) })
@ -312,69 +313,95 @@ describe('Pattern', function() {
describe('invert()', function() { describe('invert()', function() {
it('Can invert a binary pattern', function() { it('Can invert a binary pattern', function() {
assert.deepStrictEqual( assert.deepStrictEqual(
sequence(true, false, [true, false]).invert().firstCycle, sequence(true, false, [true, false]).invert().firstCycle(),
sequence(false, true, [false, true]).firstCycle sequence(false, true, [false, true]).firstCycle()
) )
}) })
}) })
describe('signal()', function() { describe('signal()', function() {
it('Can make saw/saw2', function() { it('Can make saw/saw2', function() {
assert.deepStrictEqual( assert.deepStrictEqual(
saw.struct(true,true,true,true).firstCycle, saw.struct(true,true,true,true).firstCycle(),
sequence(1/8,3/8,5/8,7/8).firstCycle sequence(1/8,3/8,5/8,7/8).firstCycle()
) )
assert.deepStrictEqual( assert.deepStrictEqual(
saw2.struct(true,true,true,true).firstCycle, saw2.struct(true,true,true,true).firstCycle(),
sequence(-3/4,-1/4,1/4,3/4).firstCycle sequence(-3/4,-1/4,1/4,3/4).firstCycle()
) )
}) })
it('Can make isaw/isaw2', function() { it('Can make isaw/isaw2', function() {
assert.deepStrictEqual( assert.deepStrictEqual(
isaw.struct(true,true,true,true).firstCycle, isaw.struct(true,true,true,true).firstCycle(),
sequence(7/8,5/8,3/8,1/8).firstCycle sequence(7/8,5/8,3/8,1/8).firstCycle()
) )
assert.deepStrictEqual( assert.deepStrictEqual(
isaw2.struct(true,true,true,true).firstCycle, isaw2.struct(true,true,true,true).firstCycle(),
sequence(3/4,1/4,-1/4,-3/4).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", () => { describe("apply", () => {
it('Can apply a function', () => { it('Can apply a function', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
sequence("a", "b")._apply(fast(2)).firstCycle, sequence("a", "b")._apply(fast(2)).firstCycle(),
sequence("a", "b").fast(2).firstCycle sequence("a", "b").fast(2).firstCycle()
) )
}), }),
it('Can apply a pattern of functions', () => { it('Can apply a pattern of functions', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
sequence("a", "b").apply(fast(2)).firstCycle, sequence("a", "b").apply(fast(2)).firstCycle(),
sequence("a", "b").fast(2).firstCycle sequence("a", "b").fast(2).firstCycle()
) )
assert.deepStrictEqual( assert.deepStrictEqual(
sequence("a", "b").apply(fast(2),fast(3)).firstCycle, sequence("a", "b").apply(fast(2),fast(3)).firstCycle(),
sequence("a", "b").fast(2,3).firstCycle sequence("a", "b").fast(2,3).firstCycle()
) )
}) })
}) })
describe("layer", () => { describe("layer", () => {
it('Can layer up multiple functions', () => { it('Can layer up multiple functions', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
sequence(1,2,3).layer(fast(2), pat => pat.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 stack(sequence(1,2,3).fast(2), sequence(1,2,3).add(3,4)).firstCycle()
) )
}) })
}) })
describe("early", () => { describe("early", () => {
it("Can shift an event earlier", () => { it("Can shift an event earlier", () => {
assert.deepStrictEqual( 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)] [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", () => { it("Can shift an event earlier, into negative time", () => {
assert.deepStrictEqual( 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)] [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", () => { describe("off", () => {
it("Can offset a transformed pattern from the original", () => { it("Can offset a transformed pattern from the original", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
pure(30).off(0.25, add(2)).firstCycle, pure(30).off(0.25, add(2)).firstCycle(),
stack(pure(30), pure(30).late(0.25).add(2)).firstCycle stack(pure(30), pure(30).late(0.25).add(2)).firstCycle()
) )
}) })
}) })
describe("jux", () => { describe("jux", () => {
it("Can juxtapose", () => { it("Can juxtapose", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
pure({a: 1}).jux(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 stack(pure({a:1, pan: 0}), pure({a:1, pan: 1}).fast(2))._sortEventsByPart().firstCycle()
) )
}) })
}) })