From a9a4cd2f5c408818172f5412599f6111f485bb3e Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 23 Feb 2022 20:18:03 +0000 Subject: [PATCH 01/21] Make pattern a function of state, not just time. --- strudel.mjs | 87 ++++++++++++++++++++++++++++--------------- test/pattern.test.mjs | 11 +++--- 2 files changed, 62 insertions(+), 36 deletions(-) diff --git a/strudel.mjs b/strudel.mjs index 25b9fd0f..839ea1c2 100644 --- a/strudel.mjs +++ b/strudel.mjs @@ -216,6 +216,27 @@ class Hap { } } +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) + } +} + class Pattern { // the following functions will get patternFactories as nested functions: constructor(query) { @@ -244,25 +265,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,13 +295,13 @@ class Pattern { } _withEvents(func) { - return new Pattern(span => func(this.query(span))) + return new Pattern(state => func(this.query(state))) } 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 @@ -287,11 +310,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() { @@ -310,9 +333,9 @@ 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) { @@ -339,10 +362,10 @@ 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) @@ -359,10 +382,10 @@ 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) @@ -377,7 +400,7 @@ class Pattern { } get firstCycle() { - return this.query(new TimeSpan(Fraction(0), Fraction(1))) + return this.query(new State(new TimeSpan(Fraction(0), Fraction(1)))) } _sortEventsByPart() { @@ -402,16 +425,16 @@ 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 ) } 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) } @@ -559,7 +582,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) { @@ -570,7 +594,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() @@ -645,8 +669,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) } @@ -668,7 +692,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) } @@ -676,7 +700,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) { @@ -687,7 +712,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() } @@ -696,10 +721,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() } diff --git a/test/pattern.test.mjs b/test/pattern.test.mjs index c6986538..73ed7957 100644 --- a/test/pattern.test.mjs +++ b/test/pattern.test.mjs @@ -2,11 +2,12 @@ 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} from "../strudel.mjs"; +import {TimeSpan, Hap, Pattern, pure, stack, fastcat, slowcat, cat, sequence, polyrhythm, silence, fast, timeCat,add,sub,mul,div, State} 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) @@ -62,7 +63,7 @@ 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 () { @@ -72,12 +73,12 @@ describe('Pattern', function() { }) 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('union()', function () { @@ -155,7 +156,7 @@ describe('Pattern', function() { 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 From 912c36d590e12dce576d1744e7d9e8c0ec809ffb Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 25 Feb 2022 10:59:33 +0000 Subject: [PATCH 02/21] Add event/hap context --- strudel.mjs | 16 +++++++++++++++- test/pattern.test.mjs | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/strudel.mjs b/strudel.mjs index 9ea35610..51003d2f 100644 --- a/strudel.mjs +++ b/strudel.mjs @@ -172,12 +172,14 @@ 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=[]) { this.whole = whole this.part = part this.value = value + this.context = context } withSpan(func) { @@ -214,6 +216,10 @@ class Hap { show() { 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 { @@ -298,6 +304,14 @@ class Pattern { 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)) + } + withLocation(location) { return this.fmap(value => { value = typeof value === 'object' && !Array.isArray(value) ? value : { value }; diff --git a/test/pattern.test.mjs b/test/pattern.test.mjs index 8db09741..008e7af9 100644 --- a/test/pattern.test.mjs +++ b/test/pattern.test.mjs @@ -9,7 +9,7 @@ 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) @@ -328,6 +328,19 @@ describe('Pattern', function() { 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, + [hap(ts(0,1), + ts(0,1), + "a", + [[[0,1],[1,2]]] + ) + ] + ) }) }) }) From fc739d12832c66e6ee0f167b8fb36af7e40245d8 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 25 Feb 2022 11:07:45 +0000 Subject: [PATCH 03/21] fix new things up to use state --- strudel.mjs | 2 +- test/pattern.test.mjs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/strudel.mjs b/strudel.mjs index 2dee5e04..044b7123 100644 --- a/strudel.mjs +++ b/strudel.mjs @@ -726,7 +726,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) } diff --git a/test/pattern.test.mjs b/test/pattern.test.mjs index 8c8b546e..ae950321 100644 --- a/test/pattern.test.mjs +++ b/test/pattern.test.mjs @@ -382,13 +382,13 @@ describe('Pattern', function() { 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)] ) }) From 29bdfb2d90637a4c0ae618efa4b0a9f315263b70 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 25 Feb 2022 11:19:12 +0000 Subject: [PATCH 04/21] _withContext() --- strudel.mjs | 4 ++++ test/pattern.test.mjs | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/strudel.mjs b/strudel.mjs index 044b7123..9d90bc57 100644 --- a/strudel.mjs +++ b/strudel.mjs @@ -312,6 +312,10 @@ class Pattern { return this._withEvent(event => event.setContext(context)) } + _withContext(func) { + return this._withEvent(event => event.setContext(func(event.context))) + } + withLocation(location) { return this.fmap(value => { value = typeof value === 'object' && !Array.isArray(value) ? value : { value }; diff --git a/test/pattern.test.mjs b/test/pattern.test.mjs index ae950321..5154ed86 100644 --- a/test/pattern.test.mjs +++ b/test/pattern.test.mjs @@ -353,6 +353,19 @@ describe('Pattern', function() { ) }) }) + 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, + [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( From f1362b1c94120e018b4ee31be3066868c07f0b8c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Feb 2022 20:10:49 +0100 Subject: [PATCH 05/21] use state query in repl + move locations to context --- repl/src/CodeMirror.tsx | 2 +- repl/src/types.d.ts | 1 + repl/src/useCycle.ts | 9 +++++---- repl/src/useRepl.ts | 4 ++-- strudel.mjs | 32 ++++++++++++++++---------------- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/repl/src/CodeMirror.tsx b/repl/src/CodeMirror.tsx index 0dc27b54..463c3fab 100644 --- a/repl/src/CodeMirror.tsx +++ b/repl/src/CodeMirror.tsx @@ -18,7 +18,7 @@ export default function CodeMirror({ value, onChange, options, editorDidMount }: } export const markEvent = (editor) => (event) => { - const locs = event.value.locations; + const locs = event.context.locations; if (!locs || !editor) { return; } diff --git a/repl/src/types.d.ts b/repl/src/types.d.ts index 8252d201..3829b99a 100644 --- a/repl/src/types.d.ts +++ b/repl/src/types.d.ts @@ -15,6 +15,7 @@ export declare interface Hap { whole: TimeSpan; part: TimeSpan; value: T; + context: any; show: () => string; } export declare interface Pattern { diff --git a/repl/src/useCycle.ts b/repl/src/useCycle.ts index bce7a8da..0ed7e4f5 100644 --- a/repl/src/useCycle.ts +++ b/repl/src/useCycle.ts @@ -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; - onQuery?: (query: TimeSpan) => Hap[]; + onQuery?: (state: State) => Hap[]; onSchedule?: (events: Hap[], cycle: number) => void; ready?: boolean; // if false, query will not be called on change props } @@ -21,7 +21,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); @@ -46,6 +46,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); }, event.part.begin.valueOf()); diff --git a/repl/src/useRepl.ts b/repl/src/useRepl.ts index e16eb331..fd742fac 100644 --- a/repl/src/useRepl.ts +++ b/repl/src/useRepl.ts @@ -81,9 +81,9 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent }: any) { [onEvent] ), onQuery: useCallback( - (span) => { + (state) => { try { - return pattern?.query(span) || []; + return pattern?.query(state) || []; } catch (err: any) { err.message = 'query error: ' + err.message; setError(err); diff --git a/strudel.mjs b/strudel.mjs index 9d90bc57..f0c1d6af 100644 --- a/strudel.mjs +++ b/strudel.mjs @@ -175,7 +175,7 @@ class Hap { The context is to store a list of source code locations causing the event */ - constructor(whole, part, value, context=[]) { + constructor(whole, part, value, context = {}) { this.whole = whole this.part = part this.value = value @@ -185,12 +185,12 @@ class Hap { 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() { @@ -317,11 +317,10 @@ class Pattern { } 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) { @@ -367,7 +366,8 @@ class Pattern { 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))))) } @@ -396,7 +396,7 @@ class Pattern { 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) haps.push(hap) } } @@ -416,7 +416,7 @@ class Pattern { 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_val.context) haps.push(hap) } } @@ -461,8 +461,9 @@ class Pattern { const pat_val = this const query = function(state) { const withWhole = function(a, b) { + // TODO: what to do with a.context here? return new Hap(choose_whole(a.whole, b.whole), b.part, - b.value + b.value, b.context ) } const match = function (a) { @@ -959,9 +960,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 { @@ -976,7 +976,7 @@ function withLocationOffset(pat, offset) { column: end.column - 1 + colOffset, }, }}); - return {...value, locations } + return {...context, locations } }); } From a3971f793c10caba218b1928185a5db19879ebfe Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Feb 2022 20:31:08 +0100 Subject: [PATCH 06/21] use context for scale --- repl/src/tonal.ts | 45 +++++++++++---------------------------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/repl/src/tonal.ts b/repl/src/tonal.ts index 4f864cd8..aee4b42d 100644 --- a/repl/src/tonal.ts +++ b/repl/src/tonal.ts @@ -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((v) => 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 }); }); }; From 08ab06c9cc45e2f2ff41ce97517b989621271a64 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Feb 2022 20:43:38 +0100 Subject: [PATCH 07/21] use context for tone --- repl/src/tone.ts | 47 +++++++++++++++++++++------------------------ repl/src/useRepl.ts | 6 +++--- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/repl/src/tone.ts b/repl/src/tone.ts index 7636f173..da59deb4 100644 --- a/repl/src/tone.ts +++ b/repl/src/tone.ts @@ -26,18 +26,17 @@ 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 { - 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 }); }); }; @@ -105,13 +104,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 }); }); }; @@ -138,8 +136,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 }, @@ -149,39 +146,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, Destination); + return event.context.getInstrument().chain(...effects, Destination); }; - 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 }); }); }; diff --git a/repl/src/useRepl.ts b/repl/src/useRepl.ts index fd742fac..4bf6b1b5 100644 --- a/repl/src/useRepl.ts +++ b/repl/src/useRepl.ts @@ -56,8 +56,9 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent }: 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); } @@ -69,7 +70,6 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent }: 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) { From 067c0bfe70e5a0b8e879d2dd0c1339eface9fdcb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Feb 2022 20:47:44 +0100 Subject: [PATCH 08/21] simplify --- repl/src/voicings.ts | 9 ++++----- strudel.mjs | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/repl/src/voicings.ts b/repl/src/voicings.ts index a5f55dc7..2b641c93 100644 --- a/repl/src/voicings.ts +++ b/repl/src/voicings.ts @@ -32,17 +32,16 @@ Pattern.prototype.voicings = function (range) { range = ['F3', 'A4']; } return this.fmapNested((event) => { - lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range); + lastVoicing = getVoicing(event.value, lastVoicing, range); return stack(...lastVoicing); }); }; 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; }); }; diff --git a/strudel.mjs b/strudel.mjs index f0c1d6af..aa0d7d8d 100644 --- a/strudel.mjs +++ b/strudel.mjs @@ -214,7 +214,7 @@ 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) { From 8b0e036ed567c2476c8c3c40606ea5b1e47a4daf Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Feb 2022 21:40:30 +0100 Subject: [PATCH 09/21] highlight voicings --- repl/src/voicings.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/repl/src/voicings.ts b/repl/src/voicings.ts index 2b641c93..fb2b6f52 100644 --- a/repl/src/voicings.ts +++ b/repl/src/voicings.ts @@ -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() ); @@ -33,7 +33,9 @@ Pattern.prototype.voicings = function (range) { } return this.fmapNested((event) => { lastVoicing = getVoicing(event.value, lastVoicing, range); - return stack(...lastVoicing); + return stack(...lastVoicing)._withContext(() => ({ + locations: event.context.locations || [], + })); }); }; From dcebdd6dbc6564602ab16edf9d0d28d8e9fe3304 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Feb 2022 21:40:48 +0100 Subject: [PATCH 10/21] fix numbers --- repl/src/shapeshifter.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/repl/src/shapeshifter.js b/repl/src/shapeshifter.js index 1c8f0e55..8c8b2b92 100644 --- a/repl/src/shapeshifter.js +++ b/repl/src/shapeshifter.js @@ -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)], From 4bea07107120af050d98410a2a4b3103e21f6144 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Feb 2022 21:41:10 +0100 Subject: [PATCH 11/21] mini: parse numbers --- repl/src/parse.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/repl/src/parse.ts b/repl/src/parse.ts index c365ccad..37a5c4dd 100644 --- a/repl/src/parse.ts +++ b/repl/src/parse.ts @@ -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': From 508767278fe19d0338abff48a4a8f2f5081ef400 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Feb 2022 21:41:33 +0100 Subject: [PATCH 12/21] merge contexts --- strudel.mjs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/strudel.mjs b/strudel.mjs index aa0d7d8d..97348cd6 100644 --- a/strudel.mjs +++ b/strudel.mjs @@ -396,7 +396,11 @@ class Pattern { 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, hap_val.context) + 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) } } @@ -416,7 +420,11 @@ class Pattern { 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, hap_val.context) + 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) } } @@ -461,10 +469,11 @@ class Pattern { const pat_val = this const query = function(state) { const withWhole = function(a, b) { - // TODO: what to do with a.context here? - return new Hap(choose_whole(a.whole, b.whole), b.part, - b.value, b.context - ) + 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(state.setSpan(a.part)).map(b => withWhole(a, b)) From 867dbf33dcec01c63c8df36ae8a61814a0ef85fb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Feb 2022 21:51:15 +0100 Subject: [PATCH 13/21] dont log --- repl/src/useRepl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repl/src/useRepl.ts b/repl/src/useRepl.ts index 4bf6b1b5..29824916 100644 --- a/repl/src/useRepl.ts +++ b/repl/src/useRepl.ts @@ -47,7 +47,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent }: 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 From aeb51a157feeff9253d0ca97cc06a0f3e4f579f9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Feb 2022 21:55:47 +0100 Subject: [PATCH 14/21] fix transpose number bug --- repl/src/tonal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repl/src/tonal.ts b/repl/src/tonal.ts index aee4b42d..0273e30f 100644 --- a/repl/src/tonal.ts +++ b/repl/src/tonal.ts @@ -52,9 +52,9 @@ Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { : String(intervalOrSemitones); if (typeof event.value === 'number') { const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval; - return event.withValue(event.value + semitones); + return event.withValue(() => event.value + semitones); } - return event.withValue((v) => Note.transpose(event.value, interval)); + return event.withValue(() => Note.transpose(event.value, interval)); }); }; From 34872b3a024918461d5db342bfcde197f94cfcae Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 26 Feb 2022 23:45:53 +0000 Subject: [PATCH 15/21] Add stateful flag to hap, add sketch of resolveState to hap. Also firstCycle now strips the context by default. --- strudel.mjs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/strudel.mjs b/strudel.mjs index 97348cd6..b239e340 100644 --- a/strudel.mjs +++ b/strudel.mjs @@ -175,11 +175,15 @@ class Hap { The context is to store a list of source code locations causing the event */ - constructor(whole, part, value, context = {}) { + 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) { @@ -199,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) @@ -316,6 +329,10 @@ class Pattern { return this._withEvent(event => event.setContext(func(event.context))) } + _stripContext() { + return this._withEvent(event => event.setContext({})) + } + withLocation(location) { return this._withContext((context) => { const locations = (context.locations || []).concat([location]) @@ -372,7 +389,7 @@ class Pattern { 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 <*> @@ -433,8 +450,12 @@ class Pattern { return new Pattern(query) } - get firstCycle() { - return this.query(new State(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() { From 0969f474044b1bf304b0c6d262ff181d7059eb15 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 26 Feb 2022 23:46:04 +0000 Subject: [PATCH 16/21] fix tests --- test/pattern.test.mjs | 172 +++++++++++++++++++++--------------------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/test/pattern.test.mjs b/test/pattern.test.mjs index 5154ed86..cc6069c8 100644 --- a/test/pattern.test.mjs +++ b/test/pattern.test.mjs @@ -9,7 +9,7 @@ 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, context=[]) => new Hap(whole, part, value, context) +const hap = (whole, part, value, context={}) => new Hap(whole, part, value, context) const third = Fraction(1,3) const twothirds = Fraction(2,3) @@ -68,7 +68,7 @@ describe('Pattern', function() { }) 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 () { @@ -83,86 +83,86 @@ describe('Pattern', function() { }) 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( @@ -182,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"), @@ -275,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"), @@ -301,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")] ) }) @@ -313,37 +313,37 @@ 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, + pure("a")._setContext([[[0,1],[1,2]]]).firstCycle(true), [hap(ts(0,1), ts(0,1), "a", @@ -356,7 +356,7 @@ describe('Pattern', function() { 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, + pure("a")._setContext([[[0,1],[1,2]]])._withContext(c => [...c,[[3,4],[3,4]]]).firstCycle(true), [hap(ts(0,1), ts(0,1), "a", @@ -369,26 +369,26 @@ describe('Pattern', function() { 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() ) }) }) @@ -409,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() ) }) }) From 4f2f00ee62929ba9cdca6d4a62ca4859223873fd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 27 Feb 2022 20:08:56 +0100 Subject: [PATCH 17/21] patterns can now be async --- repl/package-lock.json | 33 +++++++++++++++++++++++++++++++++ repl/package.json | 3 ++- repl/src/App.tsx | 8 ++++---- repl/src/evaluate.ts | 4 ++-- repl/src/useRepl.ts | 8 ++++---- 5 files changed, 45 insertions(+), 11 deletions(-) diff --git a/repl/package-lock.json b/repl/package-lock.json index 5f440701..8388628c 100644 --- a/repl/package-lock.json +++ b/repl/package-lock.json @@ -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", diff --git a/repl/package.json b/repl/package.json index fa9fcfa2..99338498 100644 --- a/repl/package.json +++ b/repl/package.json @@ -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", diff --git a/repl/src/App.tsx b/repl/src/App.tsx index 71d83ad3..d24e45fd 100644 --- a/repl/src/App.tsx +++ b/repl/src/App.tsx @@ -51,11 +51,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(); + await activateCode(); !cycle.started && cycle.start(); break; case 'Period': @@ -88,11 +88,11 @@ function App() {