mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 21:58:31 +00:00
Merge pull request #14 from tidalcycles/stateful-events
Stateful queries and events (WIP)
This commit is contained in:
commit
190729df73
33
repl/package-lock.json
generated
33
repl/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -18,7 +18,7 @@ try {
|
||||
console.warn('failed to decode', err);
|
||||
}
|
||||
// "balanced" | "interactive" | "playback";
|
||||
Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 }));
|
||||
// Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 }));
|
||||
const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.getDestination());
|
||||
defaultSynth.set({
|
||||
oscillator: { type: 'triangle' },
|
||||
@ -37,11 +37,12 @@ const randomTune = getRandomTune();
|
||||
|
||||
function App() {
|
||||
const [editor, setEditor] = useState<any>();
|
||||
const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog } = useRepl({
|
||||
tune: decoded || randomTune,
|
||||
defaultSynth,
|
||||
onDraw: useCallback(markEvent(editor), [editor]),
|
||||
});
|
||||
const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog, pending } =
|
||||
useRepl({
|
||||
tune: decoded || randomTune,
|
||||
defaultSynth,
|
||||
onDraw: useCallback(markEvent(editor), [editor]),
|
||||
});
|
||||
const logBox = useRef<any>();
|
||||
// scroll log box to bottom when log changes
|
||||
useLayoutEffect(() => {
|
||||
@ -51,12 +52,11 @@ function App() {
|
||||
// set active pattern on ctrl+enter
|
||||
useLayoutEffect(() => {
|
||||
// TODO: make sure this is only fired when editor has focus
|
||||
const handleKeyPress = (e: any) => {
|
||||
const handleKeyPress = async (e: any) => {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
switch (e.code) {
|
||||
case 'Enter':
|
||||
activateCode();
|
||||
!cycle.started && cycle.start();
|
||||
await activateCode();
|
||||
break;
|
||||
case 'Period':
|
||||
cycle.stop();
|
||||
@ -88,11 +88,11 @@ function App() {
|
||||
</div>
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
const _code = getRandomTune();
|
||||
console.log('tune', _code); // uncomment this to debug when random code fails
|
||||
setCode(_code);
|
||||
const parsed = evaluate(_code);
|
||||
const parsed = await evaluate(_code);
|
||||
// Tone.Transport.cancel(Tone.Transport.seconds);
|
||||
setPattern(parsed.pattern);
|
||||
}}
|
||||
@ -133,7 +133,7 @@ function App() {
|
||||
className="flex-none w-full border border-gray-700 p-2 bg-slate-700 hover:bg-slate-500"
|
||||
onClick={() => togglePlay()}
|
||||
>
|
||||
{cycle.started ? 'pause' : 'play'}
|
||||
{!pending ? <>{cycle.started ? 'pause' : 'play'}</> : <>loading...</>}
|
||||
</button>
|
||||
<textarea
|
||||
className="grow bg-[#283237] border-0 text-xs min-h-[200px]"
|
||||
|
||||
@ -18,7 +18,7 @@ export default function CodeMirror({ value, onChange, options, editorDidMount }:
|
||||
}
|
||||
|
||||
export const markEvent = (editor) => (time, event) => {
|
||||
const locs = event.value.locations;
|
||||
const locs = event.context.locations;
|
||||
if (!locs || !editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -29,10 +29,10 @@ hackLiteral(String, ['pure', 'p'], bootstrapped.pure); // comment out this line
|
||||
// this will add everything to global scope, which is accessed by eval
|
||||
Object.assign(globalThis, bootstrapped, Tone, toneHelpers);
|
||||
|
||||
export const evaluate: any = (code: string) => {
|
||||
export const evaluate: any = async (code: string) => {
|
||||
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
|
||||
// console.log('shapeshifted', shapeshifted);
|
||||
let evaluated = eval(shapeshifted);
|
||||
let evaluated = await eval(shapeshifted);
|
||||
if (typeof evaluated === 'function') {
|
||||
evaluated = evaluated();
|
||||
}
|
||||
|
||||
@ -101,10 +101,11 @@ export function patternifyAST(ast: any): any {
|
||||
return ast.source_;
|
||||
}
|
||||
const { start, end } = ast.location_;
|
||||
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
||||
// return ast.source_;
|
||||
// the following line expects the shapeshifter to wrap this in withLocationOffset
|
||||
// because location_ is only relative to the mini string, but we need it relative to whole code
|
||||
return pure(ast.source_).withLocation({ start, end });
|
||||
return pure(value).withLocation({ start, end });
|
||||
}
|
||||
return patternifyAST(ast.source_);
|
||||
case 'stretch':
|
||||
|
||||
@ -92,15 +92,16 @@ export default (code) => {
|
||||
// add to location to pure(x) calls
|
||||
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
|
||||
const literal = node.arguments[0];
|
||||
const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
|
||||
return reifyWithLocation(value + '', node.arguments[0], ast.locations, artificialNodes);
|
||||
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
|
||||
// console.log('value',value);
|
||||
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
|
||||
}
|
||||
// replace pseudo note variables
|
||||
if (node.type === 'IdentifierExpression') {
|
||||
if (isNote(node.name)) {
|
||||
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
|
||||
if (addLocations && isMarkable) {
|
||||
return reifyWithLocation(value, node, ast.locations, artificialNodes);
|
||||
return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes);
|
||||
}
|
||||
return new LiteralStringExpression({ value });
|
||||
}
|
||||
@ -110,7 +111,7 @@ export default (code) => {
|
||||
}
|
||||
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) {
|
||||
// console.log('add', node);
|
||||
return reifyWithLocation(node.value, node, ast.locations, artificialNodes);
|
||||
return reifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||
}
|
||||
if (!addMiniLocations) {
|
||||
return wrapFunction('reify', node);
|
||||
@ -219,10 +220,10 @@ function wrapLocationOffset(node, stringNode, locations, artificialNodes) {
|
||||
|
||||
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||
function reifyWithLocation(value, node, locations, artificialNodes) {
|
||||
function reifyWithLocation(literalNode, node, locations, artificialNodes) {
|
||||
const withLocation = new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
object: wrapFunction('reify', new LiteralStringExpression({ value })),
|
||||
object: wrapFunction('reify', literalNode),
|
||||
property: 'withLocation',
|
||||
}),
|
||||
arguments: [getLocationObject(node, locations)],
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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
1
repl/src/types.d.ts
vendored
@ -15,6 +15,7 @@ export declare interface Hap<T = any> {
|
||||
whole: TimeSpan;
|
||||
part: TimeSpan;
|
||||
value: T;
|
||||
context: any;
|
||||
show: () => string;
|
||||
}
|
||||
export declare interface Pattern<T = any> {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { ToneEventCallback } from 'tone';
|
||||
import * as Tone from 'tone';
|
||||
import { TimeSpan } from '../../strudel.mjs';
|
||||
import { TimeSpan, State } from '../../strudel.mjs';
|
||||
import type { Hap } from './types';
|
||||
|
||||
export declare interface UseCycleProps {
|
||||
onEvent: ToneEventCallback<any>;
|
||||
onQuery?: (query: TimeSpan) => Hap[];
|
||||
onQuery?: (state: State) => Hap[];
|
||||
onSchedule?: (events: Hap[], cycle: number) => void;
|
||||
onDraw?: ToneEventCallback<any>;
|
||||
ready?: boolean; // if false, query will not be called on change props
|
||||
@ -22,7 +22,7 @@ function useCycle(props: UseCycleProps) {
|
||||
// pull events with onQuery + count up to next cycle
|
||||
const query = (cycle = activeCycle()) => {
|
||||
const timespan = new TimeSpan(cycle, cycle + 1);
|
||||
const events = onQuery?.(timespan) || [];
|
||||
const events = onQuery?.(new State(timespan)) || [];
|
||||
onSchedule?.(events, cycle);
|
||||
// cancel events after current query. makes sure no old events are player for rescheduled cycles
|
||||
// console.log('schedule', cycle);
|
||||
@ -47,6 +47,7 @@ function useCycle(props: UseCycleProps) {
|
||||
time: event.part.begin.valueOf(),
|
||||
duration: event.whole.end.sub(event.whole.begin).valueOf(),
|
||||
value: event.value,
|
||||
context: event.context,
|
||||
};
|
||||
onEvent(time, toneEvent);
|
||||
Tone.Draw.schedule(() => {
|
||||
|
||||
@ -17,19 +17,22 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
const [activeCode, setActiveCode] = useState<string>();
|
||||
const [log, setLog] = useState('');
|
||||
const [error, setError] = useState<Error>();
|
||||
const [pending, setPending] = useState(false);
|
||||
const [hash, setHash] = useState('');
|
||||
const [pattern, setPattern] = useState<Pattern>();
|
||||
const dirty = code !== activeCode || error;
|
||||
const generateHash = () => encodeURIComponent(btoa(code));
|
||||
const activateCode = (_code = code) => {
|
||||
!cycle.started && cycle.start();
|
||||
broadcast({ type: 'start', from: id });
|
||||
const activateCode = async (_code = code) => {
|
||||
if (activeCode && !dirty) {
|
||||
setError(undefined);
|
||||
!cycle.started && cycle.start();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = evaluate(_code);
|
||||
setPending(true);
|
||||
const parsed = await evaluate(_code);
|
||||
!cycle.started && cycle.start();
|
||||
broadcast({ type: 'start', from: id });
|
||||
setPattern(() => parsed.pattern);
|
||||
if (autolink) {
|
||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
||||
@ -37,6 +40,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
setHash(generateHash());
|
||||
setError(undefined);
|
||||
setActiveCode(_code);
|
||||
setPending(false);
|
||||
} catch (err: any) {
|
||||
err.message = 'evaluation error: ' + err.message;
|
||||
console.warn(err);
|
||||
@ -47,7 +51,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
// logs events of cycle
|
||||
const logCycle = (_events: any, cycle: any) => {
|
||||
if (_events.length) {
|
||||
//pushLog(`# cycle ${cycle}\n` + _events.map((e: any) => e.show()).join('\n'));
|
||||
// pushLog(`# cycle ${cycle}\n` + _events.map((e: any) => e.show()).join('\n'));
|
||||
}
|
||||
};
|
||||
// cycle hook to control scheduling
|
||||
@ -57,8 +61,9 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
(time, event) => {
|
||||
try {
|
||||
onEvent?.(event);
|
||||
if (!event.value?.onTrigger) {
|
||||
const note = event.value?.value || event.value;
|
||||
const { onTrigger } = event.context;
|
||||
if (!onTrigger) {
|
||||
const note = event.value;
|
||||
if (!isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
@ -70,7 +75,6 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
/* console.warn('no instrument chosen', event);
|
||||
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
||||
} else {
|
||||
const { onTrigger } = event.value;
|
||||
onTrigger(time, event);
|
||||
}
|
||||
} catch (err: any) {
|
||||
@ -82,9 +86,9 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
[onEvent]
|
||||
),
|
||||
onQuery: useCallback(
|
||||
(span) => {
|
||||
(state) => {
|
||||
try {
|
||||
return pattern?.query(span) || [];
|
||||
return pattern?.query(state) || [];
|
||||
} catch (err: any) {
|
||||
console.warn(err);
|
||||
err.message = 'query error: ' + err.message;
|
||||
@ -146,6 +150,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any)
|
||||
};
|
||||
|
||||
return {
|
||||
pending,
|
||||
code,
|
||||
setCode,
|
||||
pattern,
|
||||
|
||||
@ -19,7 +19,7 @@ Pattern.prototype.fmapNested = function (func) {
|
||||
.map((event) =>
|
||||
reify(func(event))
|
||||
.query(span)
|
||||
.map((hap) => new Hap(event.whole, event.part, hap.value))
|
||||
.map((hap) => new Hap(event.whole, event.part, hap.value, hap.context))
|
||||
)
|
||||
.flat()
|
||||
);
|
||||
@ -32,17 +32,18 @@ Pattern.prototype.voicings = function (range) {
|
||||
range = ['F3', 'A4'];
|
||||
}
|
||||
return this.fmapNested((event) => {
|
||||
lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range);
|
||||
return stack(...lastVoicing);
|
||||
lastVoicing = getVoicing(event.value, lastVoicing, range);
|
||||
return stack(...lastVoicing)._withContext(() => ({
|
||||
locations: event.context.locations || [],
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.rootNotes = function (octave = 2) {
|
||||
// range = ['G1', 'C3']
|
||||
return this._mapNotes((value) => {
|
||||
const [_, root] = value.value.match(/^([a-gA-G])[b#]?.*$/);
|
||||
const bassNote = root + octave;
|
||||
return { ...value, value: bassNote };
|
||||
return this.fmap((value) => {
|
||||
const [_, root] = value.match(/^([a-gA-G])[b#]?.*$/);
|
||||
return root + octave;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
179
strudel.mjs
179
strudel.mjs
@ -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 }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user