From 6eec4277c18c388a489552687186bbae17f9eff1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 8 Jan 2024 23:34:12 -0500 Subject: [PATCH 01/69] playing around --- packages/core/cyclist.mjs | 60 +++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index c835ca76..9044980f 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -16,9 +16,20 @@ export class Cyclist { this.lastBegin = 0; // query begin of last tick this.lastEnd = 0; // query end of last tick this.getTime = getTime; // get absolute time - this.num_cycles_since_last_cps_change = 0; + this.num_cycles_at_cps_change = 0; this.onToggle = onToggle; this.latency = latency; // fixed trigger time offset + this.broadcast = new BroadcastChannel('strudel_clock'); + this.nextCycleStartTime = 0; + this.broadcast.onmessage = (event) => { + const data = event.data; + const { cps, sendTime, phase, nextCycleStartTime, cycle } = data; + this.cps = cps; + const now = Date.now(); + const messageLatency = now - sendTime; + console.log({ messageLatency }); + this.nextCycleStartTime = now + nextCycleStartTime - messageLatency; + }; this.clock = createClock( getTime, // called slightly before each cycle @@ -27,23 +38,25 @@ export class Cyclist { this.origin = phase; } if (this.num_ticks_since_cps_change === 0) { - this.num_cycles_since_last_cps_change = this.lastEnd; + this.num_cycles_at_cps_change = this.lastEnd; } this.num_ticks_since_cps_change++; try { const time = getTime(); const begin = this.lastEnd; this.lastBegin = begin; - + console.log(); //convert ticks to cycles, so you can query the pattern for events const eventLength = duration * this.cps; - const end = this.num_cycles_since_last_cps_change + this.num_ticks_since_cps_change * eventLength; + const num_cycles_since_cps_change = this.num_ticks_since_cps_change * eventLength; + const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; this.lastEnd = end; // query the pattern for events const haps = this.pattern.queryArc(begin, end); const tickdeadline = phase - time; // time left until the phase is a whole number + this.lastTick = time + tickdeadline; haps.forEach((hap) => { @@ -53,6 +66,16 @@ export class Cyclist { onTrigger?.(hap, deadline, duration, this.cps); } }); + console.log(1 - (num_cycles_since_cps_change % 1)); + if (tick % 1 === 0) { + // this.broadcast.postMessage({ + // cps: this.cps, + // sendTime: Date.now(), + // phase, + // nextCycleStartTime: (1 - (num_cycles_since_cps_change % 1)) * this.cps * 1000, + // cycle: num_cycles_since_cps_change, + // }); + } } catch (e) { logger(`[cyclist] error: ${e.message}`); onError?.(e); @@ -70,14 +93,27 @@ export class Cyclist { this.onToggle?.(v); } start() { - this.num_ticks_since_cps_change = 0; - this.num_cycles_since_last_cps_change = 0; - if (!this.pattern) { - throw new Error('Scheduler: no pattern set! call .setPattern first.'); - } - logger('[cyclist] start'); - this.clock.start(); - this.setStarted(true); + const date = Date.now(); + let wait = this.nextCycleStartTime - date; + wait = Math.max(0, wait); + console.log({ wait }); + + this.broadcast.postMessage({ + type: 'request_start', + }); + + this.broadcast.onmessage; + + setTimeout(() => { + this.num_ticks_since_cps_change = 0; + this.num_cycles_at_cps_change = 0; + if (!this.pattern) { + throw new Error('Scheduler: no pattern set! call .setPattern first.'); + } + logger('[cyclist] start'); + this.clock.start(); + this.setStarted(true); + }, wait); } pause() { logger('[cyclist] pause'); From 0006f6483d43d6cda9db430db2b63fd23fa89f39 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 9 Jan 2024 19:39:21 -0500 Subject: [PATCH 02/69] playing with workers --- packages/core/cyclist.mjs | 50 ++++++---------------------------- packages/core/cyclistworker.js | 17 ++++++++++++ packages/core/deep-thought.js | 5 ++++ packages/core/repl.mjs | 7 +++++ 4 files changed, 38 insertions(+), 41 deletions(-) create mode 100644 packages/core/cyclistworker.js create mode 100644 packages/core/deep-thought.js diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 9044980f..ba4f92c7 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -19,17 +19,8 @@ export class Cyclist { this.num_cycles_at_cps_change = 0; this.onToggle = onToggle; this.latency = latency; // fixed trigger time offset - this.broadcast = new BroadcastChannel('strudel_clock'); this.nextCycleStartTime = 0; - this.broadcast.onmessage = (event) => { - const data = event.data; - const { cps, sendTime, phase, nextCycleStartTime, cycle } = data; - this.cps = cps; - const now = Date.now(); - const messageLatency = now - sendTime; - console.log({ messageLatency }); - this.nextCycleStartTime = now + nextCycleStartTime - messageLatency; - }; + this.clock = createClock( getTime, // called slightly before each cycle @@ -66,16 +57,6 @@ export class Cyclist { onTrigger?.(hap, deadline, duration, this.cps); } }); - console.log(1 - (num_cycles_since_cps_change % 1)); - if (tick % 1 === 0) { - // this.broadcast.postMessage({ - // cps: this.cps, - // sendTime: Date.now(), - // phase, - // nextCycleStartTime: (1 - (num_cycles_since_cps_change % 1)) * this.cps * 1000, - // cycle: num_cycles_since_cps_change, - // }); - } } catch (e) { logger(`[cyclist] error: ${e.message}`); onError?.(e); @@ -93,27 +74,14 @@ export class Cyclist { this.onToggle?.(v); } start() { - const date = Date.now(); - let wait = this.nextCycleStartTime - date; - wait = Math.max(0, wait); - console.log({ wait }); - - this.broadcast.postMessage({ - type: 'request_start', - }); - - this.broadcast.onmessage; - - setTimeout(() => { - this.num_ticks_since_cps_change = 0; - this.num_cycles_at_cps_change = 0; - if (!this.pattern) { - throw new Error('Scheduler: no pattern set! call .setPattern first.'); - } - logger('[cyclist] start'); - this.clock.start(); - this.setStarted(true); - }, wait); + this.num_ticks_since_cps_change = 0; + this.num_cycles_at_cps_change = 0; + if (!this.pattern) { + throw new Error('Scheduler: no pattern set! call .setPattern first.'); + } + logger('[cyclist] start'); + this.clock.start(); + this.setStarted(true); } pause() { logger('[cyclist] pause'); diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js new file mode 100644 index 00000000..832c8172 --- /dev/null +++ b/packages/core/cyclistworker.js @@ -0,0 +1,17 @@ +const ports = []; + +self.onconnect = function (ev) { + let port = ev.ports[0]; + port.onmessage = (e) => { + setTimeout(() => { + ports.forEach((p) => p.postMessage([e.data, ev.ports.length])); + }, 300); + }; + port.start(); + ports.push(port); +}; +self.onmessage = ({ data: { question } }) => { + self.postMessage({ + answer: 42, + }); +}; diff --git a/packages/core/deep-thought.js b/packages/core/deep-thought.js new file mode 100644 index 00000000..5fbb36b9 --- /dev/null +++ b/packages/core/deep-thought.js @@ -0,0 +1,5 @@ +self.onmessage = ({ data: { question } }) => { + self.postMessage({ + answer: 42, + }); +}; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 9fb6b4b9..609fbb42 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -30,6 +30,13 @@ export function repl({ started: false, }; + const worker = new Worker(new URL('./deep-thought.js', import.meta.url)); + worker.postMessage({ + question: 'The Answer to the Ultimate Question of Life, The Universe, and Everything.', + }); + worker.onmessage = ({ data: { answer } }) => { + console.log(answer); + }; const updateState = (update) => { Object.assign(state, update); state.isDirty = state.code !== state.activeCode; From 19f24346942bbafdbcdd657105b6d42b917e7a3d Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 9 Jan 2024 22:49:39 -0500 Subject: [PATCH 03/69] workerizing --- packages/core/cyclistworker.js | 129 +++++++++++++++++++++++++++++---- packages/core/repl.mjs | 8 ++ 2 files changed, 124 insertions(+), 13 deletions(-) diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index 832c8172..b0028698 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -1,17 +1,120 @@ -const ports = []; +const allPorts = []; +let cps = 1; +let num_ticks_since_cps_change = 0; +let lastTick = 0; // absolute time when last tick (clock callback) happened +let lastBegin = 0; // query begin of last tick +let lastEnd = 0; // query end of last tick +// let getTime = getTime; // get absolute time +let num_cycles_at_cps_change = 0; +// let onToggle = onToggle; +let latency = 0.1; // fixed trigger time offset +let interval = 0.1; -self.onconnect = function (ev) { - let port = ev.ports[0]; - port.onmessage = (e) => { - setTimeout(() => { - ports.forEach((p) => p.postMessage([e.data, ev.ports.length])); - }, 300); - }; - port.start(); - ports.push(port); +//incoming +//cps message +// {type: 'cpschange', payload: {cps}} + +//toggle +// {type: toggle, payload?: {started: boolean}} + +//sending +//{type: 'tick', payload: {begin, end, phase, time }} +//{type: 'log', payload: {type, text}} + +const getTime = () => { + return performance.now(); }; -self.onmessage = ({ data: { question } }) => { - self.postMessage({ - answer: 42, + +const sendMessage = (message) => { + allPorts.forEach((port) => { + port.postMessage(message); }); }; +const log = (text, type) => { + sendMessage({ type: 'log', payload: { text, type } }); +}; + +createClock( + getTime, + // called slightly before each cycle + (phase, duration, tick) => { + if (num_ticks_since_cps_change === 0) { + num_cycles_at_cps_change = lastEnd; + } + num_ticks_since_cps_change++; + try { + const time = getTime(); + const begin = lastEnd; + lastBegin = begin; + //convert ticks to cycles, so you can query the pattern for events + const eventLength = duration * cps; + const num_cycles_since_cps_change = num_ticks_since_cps_change * eventLength; + const end = num_cycles_at_cps_change + num_cycles_since_cps_change; + lastEnd = end; + const tickdeadline = phase - time; // time left until the phase is a whole number + lastTick = time + tickdeadline; + sendMessage({ type: 'tick', payload: { begin, end, phase, time } }); + } catch (e) { + log(`[cyclist] error: ${e.message}`, 'error'); + } + }, + interval, // duration of each cycle +); + +self.onconnect = function (e) { + // the incoming port + var port = e.ports[0]; + allPorts.push(port); + + allPorts.forEach((port) => { + port.postMessage('yoooo'); + }); + + port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. +}; + +function createClock( + getTime, + callback, // called slightly before each cycle + duration = 0.05, // duration of each cycle + interval = 0.1, // interval between callbacks + overlap = 0.1, // overlap between callbacks +) { + let tick = 0; // counts callbacks + let phase = 0; // next callback time + let precision = 10 ** 4; // used to round phase + let minLatency = 0.01; + const setDuration = (setter) => (duration = setter(duration)); + overlap = overlap || interval / 2; + const onTick = () => { + const t = getTime(); + const lookahead = t + interval + overlap; // the time window for this tick + if (phase === 0) { + phase = t + minLatency; + } + // callback as long as we're inside the lookahead + while (phase < lookahead) { + phase = Math.round(phase * precision) / precision; + phase >= t && callback(phase, duration, tick); + phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? + phase += duration; // increment phase by duration + tick++; + } + }; + let intervalID; + const start = () => { + clear(); // just in case start was called more than once + onTick(); + intervalID = setInterval(onTick, interval * 1000); + }; + const clear = () => intervalID !== undefined && clearInterval(intervalID); + const pause = () => clear(); + const stop = () => { + tick = 0; + phase = 0; + clear(); + }; + const getPhase = () => phase; + // setCallback + return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; +} diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 609fbb42..245b651f 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -37,6 +37,14 @@ export function repl({ worker.onmessage = ({ data: { answer } }) => { console.log(answer); }; + + const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); + + sharedworker.port.start(); + sharedworker.port.addEventListener('message', (message) => { + console.log(message); + }); + const updateState = (update) => { Object.assign(state, update); state.isDirty = state.code !== state.activeCode; From d329ccc4e3a5f0888f18e813be9f04f601cfc523 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 10 Jan 2024 01:40:19 -0500 Subject: [PATCH 04/69] converting --- packages/core/cyclistworker.js | 36 +++++++++++++++-- packages/core/neocyclist.mjs | 73 ++++++++++++++++++++++++++++++++++ packages/core/repl.mjs | 15 ++++++- 3 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 packages/core/neocyclist.mjs diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index b0028698..56af5531 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -7,7 +7,6 @@ let lastEnd = 0; // query end of last tick // let getTime = getTime; // get absolute time let num_cycles_at_cps_change = 0; // let onToggle = onToggle; -let latency = 0.1; // fixed trigger time offset let interval = 0.1; //incoming @@ -18,7 +17,7 @@ let interval = 0.1; // {type: toggle, payload?: {started: boolean}} //sending -//{type: 'tick', payload: {begin, end, phase, time }} +//{type: 'tick', payload: {begin, end, deadline }} //{type: 'log', payload: {type, text}} const getTime = () => { @@ -34,7 +33,7 @@ const log = (text, type) => { sendMessage({ type: 'log', payload: { text, type } }); }; -createClock( +let clock = createClock( getTime, // called slightly before each cycle (phase, duration, tick) => { @@ -53,7 +52,7 @@ createClock( lastEnd = end; const tickdeadline = phase - time; // time left until the phase is a whole number lastTick = time + tickdeadline; - sendMessage({ type: 'tick', payload: { begin, end, phase, time } }); + sendMessage({ type: 'tick', payload: { begin, end, tickdeadline } }); } catch (e) { log(`[cyclist] error: ${e.message}`, 'error'); } @@ -73,6 +72,33 @@ self.onconnect = function (e) { port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; +self.onmessage = (message) => { + const { type, payload } = message; + switch (type) { + case 'cpschange': { + if (payload.cps !== cps) { + cps = payload.cps; + num_ticks_since_cps_change = 0; + } + break; + } + case 'toggle': { + const { started } = payload; + if (started) { + clock.start(); + } else { + clock.stop(); + } + break; + } + case 'requestcycles': { + const secondsSinceLastTick = getTime() - lastTick - clock.duration; + const cycles = this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; + sendMessage({ type: 'requestedcycles', payload: { cycles } }); + } + } +}; + function createClock( getTime, callback, // called slightly before each cycle @@ -84,6 +110,7 @@ function createClock( let phase = 0; // next callback time let precision = 10 ** 4; // used to round phase let minLatency = 0.01; + const setDuration = (setter) => (duration = setter(duration)); overlap = overlap || interval / 2; const onTick = () => { @@ -115,6 +142,7 @@ function createClock( clear(); }; const getPhase = () => phase; + // setCallback return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; } diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs new file mode 100644 index 00000000..da2b7794 --- /dev/null +++ b/packages/core/neocyclist.mjs @@ -0,0 +1,73 @@ +import { logger } from './logger.mjs'; + +const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); +sharedworker.port.start(); + +export class NeoCyclist { + constructor({ onTrigger, onToggle, latency = 0.1, onError }) { + this.started = false; + this.pattern; + this.onToggle = onToggle; + this.latency = latency; + this.worker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); + this.worker.port.addEventListener('message', (message) => { + const { payload, type } = message; + switch (type) { + case 'tick': { + console.log('tick'); + const { begin, end } = payload; + const haps = this.pattern.queryArc(begin, end); + haps.forEach((hap) => { + if (hap.part.begin.equals(hap.whole.begin)) { + const deadline = (hap.whole.begin - begin) / this.cps + payload.deadline + latency; + const duration = hap.duration / this.cps; + onTrigger?.(hap, deadline, duration, this.cps); + } + }); + break; + } + case 'log': { + const { type, text } = payload; + if (type == 'error') { + onError(text); + } else { + logger(text, type); + } + } + } + }); + } + sendMessage(type, payload) { + this.worker.port.postMessage({ type, payload }); + } + + now() { + this.sendMessage('requestcycles', {}); + } + setCps(cps = 1) { + this.sendMessage('cpschange', { cps }); + } + setStarted(started) { + this.sendMessage('toggle', { started }); + this.started = started; + this.onToggle?.(started); + } + start() { + logger('[cyclist] start'); + this.setStarted(true); + } + stop() { + logger('[cyclist] stop'); + this.setStarted(false); + } + setPattern(pat, autostart = false) { + this.pattern = pat; + if (autostart && !this.started) { + this.start(); + } + } + log(begin, end, haps) { + const onsets = haps.filter((h) => h.hasOnset()); + console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); + } +} diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 245b651f..b3b810c0 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -4,6 +4,7 @@ import { logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; +import { NeoCyclist } from './neocyclist.mjs'; export function repl({ interval, @@ -52,11 +53,21 @@ export function repl({ onUpdateState?.(state); }; - const scheduler = new Cyclist({ + // const scheduler = new Cyclist({ + // interval, + // onTrigger: getTrigger({ defaultOutput, getTime }), + // onError: onSchedulerError, + // getTime, + // onToggle: (started) => { + // updateState({ started }); + // onToggle?.(started); + // }, + // }); + + const scheduler = new NeoCyclist({ interval, onTrigger: getTrigger({ defaultOutput, getTime }), onError: onSchedulerError, - getTime, onToggle: (started) => { updateState({ started }); onToggle?.(started); From f1eaa83af5d2450b83ee82bec7934d560500106a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 10 Jan 2024 14:43:18 -0500 Subject: [PATCH 05/69] trying stuff --- packages/core/cyclistworker.js | 12 +++++++++--- packages/core/neocyclist.mjs | 9 ++++++--- packages/core/repl.mjs | 10 +++++----- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index 56af5531..45f6a729 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -65,15 +65,21 @@ self.onconnect = function (e) { var port = e.ports[0]; allPorts.push(port); - allPorts.forEach((port) => { - port.postMessage('yoooo'); + sendMessage('yooooo'); + + port.addEventListener('message', function (e) { + // get the message sent to the worker + + processMessage(e.data); }); port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; -self.onmessage = (message) => { +const processMessage = (message) => { + console.log(message); const { type, payload } = message; + switch (type) { case 'cpschange': { if (payload.cps !== cps) { diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index da2b7794..302616e7 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -1,7 +1,7 @@ import { logger } from './logger.mjs'; -const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); -sharedworker.port.start(); +// const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); +// sharedworker.port.start(); export class NeoCyclist { constructor({ onTrigger, onToggle, latency = 0.1, onError }) { @@ -10,7 +10,9 @@ export class NeoCyclist { this.onToggle = onToggle; this.latency = latency; this.worker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); + this.worker.port.start(); this.worker.port.addEventListener('message', (message) => { + console.log(message); const { payload, type } = message; switch (type) { case 'tick': { @@ -42,7 +44,8 @@ export class NeoCyclist { } now() { - this.sendMessage('requestcycles', {}); + return performance.now(); + // this.sendMessage('requestcycles', {}); } setCps(cps = 1) { this.sendMessage('cpschange', { cps }); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index b3b810c0..96b62e77 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -39,12 +39,12 @@ export function repl({ console.log(answer); }; - const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); + // const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); - sharedworker.port.start(); - sharedworker.port.addEventListener('message', (message) => { - console.log(message); - }); + // sharedworker.port.start(); + // sharedworker.port.addEventListener('message', (message) => { + // console.log(message); + // }); const updateState = (update) => { Object.assign(state, update); From 0c7b731fa62119b8a160a8b6ade31c5264c44570 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 10 Jan 2024 20:05:27 -0500 Subject: [PATCH 06/69] latency :( --- packages/core/cyclistworker.js | 6 +++--- packages/core/neocyclist.mjs | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index 45f6a729..b6b74ffb 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -21,7 +21,7 @@ let interval = 0.1; //{type: 'log', payload: {type, text}} const getTime = () => { - return performance.now(); + return performance.now() / 1000; }; const sendMessage = (message) => { @@ -52,7 +52,7 @@ let clock = createClock( lastEnd = end; const tickdeadline = phase - time; // time left until the phase is a whole number lastTick = time + tickdeadline; - sendMessage({ type: 'tick', payload: { begin, end, tickdeadline } }); + sendMessage({ type: 'tick', payload: { begin, end, tickdeadline, cps, time: Date.now() } }); } catch (e) { log(`[cyclist] error: ${e.message}`, 'error'); } @@ -77,7 +77,6 @@ self.onconnect = function (e) { }; const processMessage = (message) => { - console.log(message); const { type, payload } = message; switch (type) { @@ -125,6 +124,7 @@ function createClock( if (phase === 0) { phase = t + minLatency; } + console.log({ t, phase, tick }); // callback as long as we're inside the lookahead while (phase < lookahead) { phase = Math.round(phase * precision) / precision; diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 302616e7..da57f6f4 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -12,18 +12,20 @@ export class NeoCyclist { this.worker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); this.worker.port.start(); this.worker.port.addEventListener('message', (message) => { - console.log(message); - const { payload, type } = message; + const { payload, type } = message.data; + switch (type) { case 'tick': { - console.log('tick'); - const { begin, end } = payload; + let { begin, end, cps, tickdeadline, time } = payload; + const messageLatency = (Date.now() - time) / 1000; + tickdeadline = tickdeadline - messageLatency; + console.log({ begin, end }); const haps = this.pattern.queryArc(begin, end); haps.forEach((hap) => { if (hap.part.begin.equals(hap.whole.begin)) { - const deadline = (hap.whole.begin - begin) / this.cps + payload.deadline + latency; - const duration = hap.duration / this.cps; - onTrigger?.(hap, deadline, duration, this.cps); + const deadline = (hap.whole.begin - begin) / cps + tickdeadline + latency; + const duration = hap.duration / cps; + onTrigger?.(hap, deadline, duration, cps); } }); break; @@ -44,7 +46,7 @@ export class NeoCyclist { } now() { - return performance.now(); + return performance.now() / 1000; // this.sendMessage('requestcycles', {}); } setCps(cps = 1) { From 721f707c945448d1135ad2929dad307ea8ede425 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 10 Jan 2024 23:49:02 -0500 Subject: [PATCH 07/69] working but animation is weird --- packages/core/cyclist.mjs | 1 - packages/core/cyclistworker.js | 48 +++++++++++++++++----------------- packages/core/deep-thought.js | 5 ---- packages/core/draw.mjs | 1 + packages/core/index.mjs | 1 + packages/core/neocyclist.mjs | 18 +++++++++---- packages/core/repl.mjs | 19 +++----------- packages/webaudio/webaudio.mjs | 7 ++++- 8 files changed, 48 insertions(+), 52 deletions(-) delete mode 100644 packages/core/deep-thought.js diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index ba4f92c7..4cce03ee 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -36,7 +36,6 @@ export class Cyclist { const time = getTime(); const begin = this.lastEnd; this.lastBegin = begin; - console.log(); //convert ticks to cycles, so you can query the pattern for events const eventLength = duration * this.cps; const num_cycles_since_cps_change = this.num_ticks_since_cps_change * eventLength; diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index b6b74ffb..6d7f0060 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -4,10 +4,9 @@ let num_ticks_since_cps_change = 0; let lastTick = 0; // absolute time when last tick (clock callback) happened let lastBegin = 0; // query begin of last tick let lastEnd = 0; // query end of last tick -// let getTime = getTime; // get absolute time let num_cycles_at_cps_change = 0; -// let onToggle = onToggle; let interval = 0.1; +let started = false; //incoming //cps message @@ -17,22 +16,30 @@ let interval = 0.1; // {type: toggle, payload?: {started: boolean}} //sending -//{type: 'tick', payload: {begin, end, deadline }} +//{type: 'tick', payload: {begin, end, tickdeadline, cps, time }} //{type: 'log', payload: {type, text}} const getTime = () => { return performance.now() / 1000; }; -const sendMessage = (message) => { +const sendMessage = (type, payload) => { allPorts.forEach((port) => { - port.postMessage(message); + port.postMessage({ type, payload }); }); }; const log = (text, type) => { - sendMessage({ type: 'log', payload: { text, type } }); + sendMessage('log', { text, type }); }; +const numClientsConnected = () => allPorts.length; + +const getCycle = () => { + const secondsSinceLastTick = getTime() - lastTick - clock.duration; + const cycle = lastBegin + secondsSinceLastTick * cps; + return cycle; +}; +// let prevtime = 0; let clock = createClock( getTime, // called slightly before each cycle @@ -41,6 +48,9 @@ let clock = createClock( num_cycles_at_cps_change = lastEnd; } num_ticks_since_cps_change++; + // const now = Date.now(); + // console.log('interval', now - prevtime); + // prevtime = now; try { const time = getTime(); const begin = lastEnd; @@ -52,7 +62,7 @@ let clock = createClock( lastEnd = end; const tickdeadline = phase - time; // time left until the phase is a whole number lastTick = time + tickdeadline; - sendMessage({ type: 'tick', payload: { begin, end, tickdeadline, cps, time: Date.now() } }); + sendMessage('tick', { begin, end, tickdeadline, cps, time: Date.now(), cycle: getCycle() }); } catch (e) { log(`[cyclist] error: ${e.message}`, 'error'); } @@ -62,17 +72,11 @@ let clock = createClock( self.onconnect = function (e) { // the incoming port - var port = e.ports[0]; + const port = e.ports[0]; allPorts.push(port); - - sendMessage('yooooo'); - port.addEventListener('message', function (e) { - // get the message sent to the worker - processMessage(e.data); }); - port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; @@ -88,19 +92,16 @@ const processMessage = (message) => { break; } case 'toggle': { - const { started } = payload; - if (started) { + if (payload.started && !started) { + started = true; clock.start(); - } else { + //dont stop the clock if others are using it... + } else if (numClientsConnected() === 1) { + started = false; clock.stop(); } break; } - case 'requestcycles': { - const secondsSinceLastTick = getTime() - lastTick - clock.duration; - const cycles = this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; - sendMessage({ type: 'requestedcycles', payload: { cycles } }); - } } }; @@ -124,7 +125,7 @@ function createClock( if (phase === 0) { phase = t + minLatency; } - console.log({ t, phase, tick }); + // console.log({ t, phase, tick }); // callback as long as we're inside the lookahead while (phase < lookahead) { phase = Math.round(phase * precision) / precision; @@ -149,6 +150,5 @@ function createClock( }; const getPhase = () => phase; - // setCallback return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; } diff --git a/packages/core/deep-thought.js b/packages/core/deep-thought.js deleted file mode 100644 index 5fbb36b9..00000000 --- a/packages/core/deep-thought.js +++ /dev/null @@ -1,5 +0,0 @@ -self.onmessage = ({ data: { question } }) => { - self.postMessage({ - answer: 42, - }); -}; diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 941401da..6e4b13de 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -124,6 +124,7 @@ export class Drawer { const lookahead = this.drawTime[1]; // calculate current frame time (think right side of screen for pianoroll) const phase = this.scheduler.now() + lookahead; + // first frame just captures the phase if (this.lastFrame === null) { this.lastFrame = phase; diff --git a/packages/core/index.mjs b/packages/core/index.mjs index f4598f11..bcbf649f 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -19,6 +19,7 @@ export * from './speak.mjs'; export * from './evaluate.mjs'; export * from './repl.mjs'; export * from './cyclist.mjs'; +export * from './neocyclist.mjs'; export * from './logger.mjs'; export * from './time.mjs'; export * from './draw.mjs'; diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index da57f6f4..03bced84 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -11,15 +11,22 @@ export class NeoCyclist { this.latency = latency; this.worker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); this.worker.port.start(); + this.cycle = 0; + this.cps = 1; this.worker.port.addEventListener('message', (message) => { + if (!this.started) { + return; + } const { payload, type } = message.data; switch (type) { case 'tick': { - let { begin, end, cps, tickdeadline, time } = payload; - const messageLatency = (Date.now() - time) / 1000; - tickdeadline = tickdeadline - messageLatency; - console.log({ begin, end }); + let { begin, end, cps, tickdeadline, time, cycle } = payload; + this.cps = cps; + this.cycle = cycle + latency * cps; + // const messageLatency = (Date.now() - time) / 1000; + // latency = latency - messageLatency + const haps = this.pattern.queryArc(begin, end); haps.forEach((hap) => { if (hap.part.begin.equals(hap.whole.begin)) { @@ -46,7 +53,8 @@ export class NeoCyclist { } now() { - return performance.now() / 1000; + // console.log(this.cycle, 'cycle'); + return this.cycle; // this.sendMessage('requestcycles', {}); } setCps(cps = 1) { diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 96b62e77..e8568351 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -31,21 +31,6 @@ export function repl({ started: false, }; - const worker = new Worker(new URL('./deep-thought.js', import.meta.url)); - worker.postMessage({ - question: 'The Answer to the Ultimate Question of Life, The Universe, and Everything.', - }); - worker.onmessage = ({ data: { answer } }) => { - console.log(answer); - }; - - // const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); - - // sharedworker.port.start(); - // sharedworker.port.addEventListener('message', (message) => { - // console.log(message); - // }); - const updateState = (update) => { Object.assign(state, update); state.isDirty = state.code !== state.activeCode; @@ -65,14 +50,16 @@ export function repl({ // }); const scheduler = new NeoCyclist({ - interval, + // interval, onTrigger: getTrigger({ defaultOutput, getTime }), onError: onSchedulerError, + // latency: 0.22, onToggle: (started) => { updateState({ started }); onToggle?.(started); }, }); + let pPatterns = {}; let allTransform; diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index fb4a3d7d..fe098fcc 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -30,7 +30,12 @@ export function webaudioScheduler(options = {}) { ...options, }; const { defaultOutput, getTime } = options; - return new strudel.Cyclist({ + // return new strudel.Cyclist({ + // ...options, + // onTrigger: strudel.getTrigger({ defaultOutput, getTime }), + // }); + console.log('here'); + return new strudel.NeoCyclist({ ...options, onTrigger: strudel.getTrigger({ defaultOutput, getTime }), }); From 4d9292226320e5bb44ae01e2a4b9efa628cc0396 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 12 Jan 2024 00:20:20 -0500 Subject: [PATCH 08/69] fixed animation drops --- packages/core/cyclistworker.js | 2 +- packages/core/draw.mjs | 2 ++ packages/core/neocyclist.mjs | 7 +++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index 6d7f0060..e4f953d6 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -62,7 +62,7 @@ let clock = createClock( lastEnd = end; const tickdeadline = phase - time; // time left until the phase is a whole number lastTick = time + tickdeadline; - sendMessage('tick', { begin, end, tickdeadline, cps, time: Date.now(), cycle: getCycle() }); + sendMessage('tick', { begin, end, tickdeadline, cps, cycle: getCycle() }); } catch (e) { log(`[cyclist] error: ${e.message}`, 'error'); } diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 6e4b13de..72b7c8b5 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -130,6 +130,7 @@ export class Drawer { this.lastFrame = phase; return; } + // query haps from last frame till now. take last 100ms max const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase); this.lastFrame = phase; @@ -154,6 +155,7 @@ export class Drawer { return; } // TODO: scheduler.now() seems to move even when it's stopped, this hints at a bug... + t = t ?? scheduler.now(); this.scheduler = scheduler; let [_, lookahead] = this.drawTime; diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 03bced84..9c6d838d 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -13,6 +13,7 @@ export class NeoCyclist { this.worker.port.start(); this.cycle = 0; this.cps = 1; + this.timeAtLastTick = 0; this.worker.port.addEventListener('message', (message) => { if (!this.started) { return; @@ -21,7 +22,8 @@ export class NeoCyclist { switch (type) { case 'tick': { - let { begin, end, cps, tickdeadline, time, cycle } = payload; + this.timeAtLastTickMessage = performance.now(); + let { begin, end, cps, tickdeadline, cycle } = payload; this.cps = cps; this.cycle = cycle + latency * cps; // const messageLatency = (Date.now() - time) / 1000; @@ -54,7 +56,8 @@ export class NeoCyclist { now() { // console.log(this.cycle, 'cycle'); - return this.cycle; + const gap = ((performance.now() - this.timeAtLastTickMessage) / 1000) * this.cps; + return this.cycle + gap; // this.sendMessage('requestcycles', {}); } setCps(cps = 1) { From 23867822803d0544b1ff78072b3f9798881a589d Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 12 Jan 2024 00:24:23 -0500 Subject: [PATCH 09/69] cleaning... --- packages/core/neocyclist.mjs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 9c6d838d..3f8d4a83 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -1,8 +1,5 @@ import { logger } from './logger.mjs'; -// const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); -// sharedworker.port.start(); - export class NeoCyclist { constructor({ onTrigger, onToggle, latency = 0.1, onError }) { this.started = false; @@ -22,12 +19,15 @@ export class NeoCyclist { switch (type) { case 'tick': { - this.timeAtLastTickMessage = performance.now(); + const now = performance.now(); + // const interval = 0.1; + // const timeSinceLastMessage = now - this.timeAtLastTickMessage; + // const messageLag = (interval * 1000 - timeSinceLastMessage) / 1000; + + this.timeAtLastTickMessage = now; let { begin, end, cps, tickdeadline, cycle } = payload; this.cps = cps; this.cycle = cycle + latency * cps; - // const messageLatency = (Date.now() - time) / 1000; - // latency = latency - messageLatency const haps = this.pattern.queryArc(begin, end); haps.forEach((hap) => { @@ -55,10 +55,8 @@ export class NeoCyclist { } now() { - // console.log(this.cycle, 'cycle'); const gap = ((performance.now() - this.timeAtLastTickMessage) / 1000) * this.cps; return this.cycle + gap; - // this.sendMessage('requestcycles', {}); } setCps(cps = 1) { this.sendMessage('cpschange', { cps }); From ccb42c831c9015990ed218b755270d4a20de5ea8 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 12 Jan 2024 18:21:23 -0500 Subject: [PATCH 10/69] cleaning --- packages/core/cyclistworker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index e4f953d6..2357c0df 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -125,7 +125,7 @@ function createClock( if (phase === 0) { phase = t + minLatency; } - // console.log({ t, phase, tick }); + // callback as long as we're inside the lookahead while (phase < lookahead) { phase = Math.round(phase * precision) / precision; From 90624abf2e2d1f9b3ad7a7b8e7725f1cd4b10ddb Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 13:54:40 -0500 Subject: [PATCH 11/69] added worker, cleaned up, added setcps function --- packages/core/clockworker.mjs | 129 ++++++++++++++ packages/core/cyclist.mjs | 301 +++++++++++++++++++++++++-------- packages/core/cyclistworker.js | 154 ----------------- packages/core/repl.mjs | 140 ++++++++------- packages/core/zyklus.mjs | 49 ------ 5 files changed, 426 insertions(+), 347 deletions(-) create mode 100644 packages/core/clockworker.mjs delete mode 100644 packages/core/cyclistworker.js delete mode 100644 packages/core/zyklus.mjs diff --git a/packages/core/clockworker.mjs b/packages/core/clockworker.mjs new file mode 100644 index 00000000..906e914f --- /dev/null +++ b/packages/core/clockworker.mjs @@ -0,0 +1,129 @@ +function getTime(precision) { + const seconds = performance.now() / 1000; + return Math.round(seconds * precision) / precision; +} +const allPorts = []; +let num_cycles_at_cps_change = 0; +let num_ticks_since_cps_change = 0; +let cps = 0.5; +const duration = 0.1; + +const sendMessage = (type, payload) => { + allPorts.forEach((port) => { + port.postMessage({ type, payload }); + }); +}; + +const sendTick = ({ phase, duration, time }) => { + sendMessage('tick', { + phase, + duration, + time, + cps, + num_cycles_at_cps_change, + num_ticks_since_cps_change, + }); + num_ticks_since_cps_change++; +}; + +const clock = createClock(sendTick, duration); +let started = false; + +const startClock = () => { + if (started) { + return; + } + clock.start(); + started = true; +}; +const stopClock = () => { + //dont stop the clock if mutliple instances are using it... + if (!started || numClientsConnected() > 1) { + return; + } + clock.stop(); + setCycle(0); + started = false; +}; + +const setCycle = (cycle) => { + num_ticks_since_cps_change = 0; + num_cycles_at_cps_change = cycle; +}; + +const numClientsConnected = () => allPorts.length; +const processMessage = (message) => { + const { type, payload } = message; + + switch (type) { + case 'cpschange': { + if (payload.cps !== cps) { + num_cycles_at_cps_change = num_cycles_at_cps_change + num_ticks_since_cps_change * duration * cps; + cps = payload.cps; + num_ticks_since_cps_change = 0; + } + break; + } + case 'setcycle': { + setCycle(payload.cycle); + break; + } + case 'toggle': { + if (payload.started) { + startClock(); + } else { + stopClock(); + } + break; + } + } +}; + +self.onconnect = function (e) { + // the incoming port + const port = e.ports[0]; + allPorts.push(port); + port.addEventListener('message', function (e) { + processMessage(e.data); + }); + port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. +}; + +function createClock( + callback, // called slightly before each cycle + duration, +) { + const interval = 0.1; + const overlap = interval / 2; + const precision = 10 ** 4; // used to round phase + const minLatency = 0.01; + let phase = 0; // next callback time + + const onTick = () => { + const t = getTime(precision); + const lookahead = t + interval + overlap; // the time window for this tick + if (phase === 0) { + phase = t + minLatency; + } + // callback as long as we're inside the lookahead + while (phase < lookahead) { + phase = Math.round(phase * precision) / precision; + phase >= t && callback({ phase, duration, time: t }); + phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? + phase += duration; // increment phase by duration + } + }; + let intervalID; + const start = () => { + clear(); // just in case start was called more than once + onTick(); + intervalID = setInterval(onTick, interval * 1000); + }; + const clear = () => intervalID !== undefined && clearInterval(intervalID); + const stop = () => { + phase = 0; + clear(); + }; + + return { start, stop }; +} diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 2afbe9c7..f44b015e 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -1,96 +1,133 @@ /* -cyclist.mjs - +cyclist.mjs - recieves clock pulses from clockworker, and schedules the next events Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; export class Cyclist { - constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) { + constructor({ onTrigger, onToggle, getTime }) { this.started = false; this.cps = 0.5; - this.num_ticks_since_cps_change = 0; this.lastTick = 0; // absolute time when last tick (clock callback) happened - this.lastBegin = 0; // query begin of last tick - this.lastEnd = 0; // query end of last tick this.getTime = getTime; // get absolute time + this.num_cycles_at_cps_change = 0; this.onToggle = onToggle; - this.latency = latency; // fixed trigger time offset - this.nextCycleStartTime = 0; + this.latency = 0.1; // fixed trigger time offset + this.cycle = 0; - this.clock = createClock( - getTime, - // called slightly before each cycle - (phase, duration, tick) => { - if (tick === 0) { - this.origin = phase; + this.worker = new SharedWorker(new URL('./clockworker.mjs', import.meta.url)); + this.worker.port.start(); + let worker_time_dif = 0; // time difference between audio context clock and worker clock + let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif + const maxWeight = 400; + const precision = 10 ** 3; //round off time diff to prevent accumulating outliers + + // the clock of the worker and the audio context clock can drift apart over time + // aditionally, the message time of the worker pinging the callback to process haps can be inconsistent. + // we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock + // in order to schedule events consistently. + const setTimeReference = (time, workertime) => { + const time_dif = workertime - time; + if (worker_time_dif === 0) { + worker_time_dif = time_dif; + } else { + const w = 1; //weight of new time diff; + const new_dif = Math.round(((worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision; + + if (new_dif != worker_time_dif) { + // reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens + weight = 4; } - if (this.num_ticks_since_cps_change === 0) { - this.num_cycles_at_cps_change = this.lastEnd; + worker_time_dif = new_dif; + } + }; + + const getTickDeadline = (phase, time) => { + return phase - time - worker_time_dif; + }; + + const tickCallback = (payload) => { + const workertime = payload.time; + const time = this.getTime(); + const { duration, phase, num_ticks_since_cps_change, num_cycles_at_cps_change, cps } = payload; + setTimeReference(time, workertime); + this.cps = cps; + + //calculate begin and end + const eventLength = duration * cps; + const num_cycles_since_cps_change = num_ticks_since_cps_change * eventLength; + const begin = num_cycles_at_cps_change + num_cycles_since_cps_change; + const tickdeadline = getTickDeadline(phase, time); + const end = begin + eventLength; + + //calculate current cycle + const lastTick = time + tickdeadline; + const secondsSinceLastTick = time - lastTick - duration; + this.cycle = begin + secondsSinceLastTick * cps; + + //set the weight of average time diff and processs haps + weight = Math.min(weight + 1, maxWeight); + processHaps(begin, end, tickdeadline); + this.time_at_last_tick_message = this.getTime(); + }; + + const processHaps = (begin, end, tickdeadline) => { + if (this.started === false) { + return; + } + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + + haps.forEach((hap) => { + if (hap.part.begin.equals(hap.whole.begin)) { + const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + this.latency; + const duration = hap.duration / this.cps; + onTrigger?.(hap, deadline, duration, this.cps); } - this.num_ticks_since_cps_change++; - try { - const time = getTime(); - const begin = this.lastEnd; - this.lastBegin = begin; - //convert ticks to cycles, so you can query the pattern for events - const eventLength = duration * this.cps; - const num_cycles_since_cps_change = this.num_ticks_since_cps_change * eventLength; - const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; - this.lastEnd = end; + }); + }; - // query the pattern for events - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + // receive messages from worker clock and process them + this.worker.port.addEventListener('message', (message) => { + if (!this.started) { + return; + } + const { payload, type } = message.data; - const tickdeadline = phase - time; // time left until the phase is a whole number - - this.lastTick = time + tickdeadline; - - haps.forEach((hap) => { - if (hap.part.begin.equals(hap.whole.begin)) { - const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency; - const duration = hap.duration / this.cps; - onTrigger?.(hap, deadline, duration, this.cps); - } - }); - } catch (e) { - logger(`[cyclist] error: ${e.message}`); - onError?.(e); + switch (type) { + case 'tick': { + tickCallback(payload); } - }, - interval, // duration of each cycle - ); + } + }); } + sendMessage(type, payload) { + this.worker.port.postMessage({ type, payload }); + } + now() { - const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; - return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; + const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps; + return this.cycle + gap; } - setStarted(v) { - this.started = v; - this.onToggle?.(v); + setCps(cps = 1) { + this.sendMessage('cpschange', { cps }); + } + setCycle(cycle) { + this.sendMessage('setcycle', { cycle }); + } + setStarted(started) { + this.sendMessage('toggle', { started }); + this.started = started; + this.onToggle?.(started); } start() { - this.num_ticks_since_cps_change = 0; - this.num_cycles_at_cps_change = 0; - if (!this.pattern) { - throw new Error('Scheduler: no pattern set! call .setPattern first.'); - } logger('[cyclist] start'); - this.clock.start(); this.setStarted(true); } - pause() { - logger('[cyclist] pause'); - this.clock.pause(); - this.setStarted(false); - } stop() { logger('[cyclist] stop'); - this.clock.stop(); - this.lastEnd = 0; this.setStarted(false); } setPattern(pat, autostart = false) { @@ -99,15 +136,139 @@ export class Cyclist { this.start(); } } - setCps(cps = 0.5) { - if (this.cps === cps) { - return; - } - this.cps = cps; - this.num_ticks_since_cps_change = 0; - } + log(begin, end, haps) { const onsets = haps.filter((h) => h.hasOnset()); console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); } } + +function getTime(precision) { + const seconds = performance.now() / 1000; + return Math.round(seconds * precision) / precision; +} +const allPorts = []; +let num_cycles_at_cps_change = 0; +let num_ticks_since_cps_change = 0; +let cps = 0.5; +const duration = 0.1; + +const sendMessage = (type, payload) => { + allPorts.forEach((port) => { + port.postMessage({ type, payload }); + }); +}; + +const sendTick = ({ phase, duration, time }) => { + sendMessage('tick', { + phase, + duration, + time, + cps, + num_cycles_at_cps_change, + num_ticks_since_cps_change, + }); + num_ticks_since_cps_change++; +}; + +const clock = createClock(sendTick, duration); +let started = false; + +const startClock = () => { + if (started) { + return; + } + clock.start(); + started = true; +}; +const stopClock = () => { + //dont stop the clock if mutliple instances are using it... + if (!started || numClientsConnected() > 1) { + return; + } + clock.stop(); + setCycle(0); + started = false; +}; + +const setCycle = (cycle) => { + num_ticks_since_cps_change = 0; + num_cycles_at_cps_change = cycle; +}; + +const numClientsConnected = () => allPorts.length; +const processMessage = (message) => { + const { type, payload } = message; + + switch (type) { + case 'cpschange': { + if (payload.cps !== cps) { + num_cycles_at_cps_change = num_cycles_at_cps_change + num_ticks_since_cps_change * duration * cps; + cps = payload.cps; + num_ticks_since_cps_change = 0; + } + break; + } + case 'setcycle': { + setCycle(payload.cycle); + break; + } + case 'toggle': { + if (payload.started) { + startClock(); + } else { + stopClock(); + } + break; + } + } +}; + +self.onconnect = function (e) { + // the incoming port + const port = e.ports[0]; + allPorts.push(port); + port.addEventListener('message', function (e) { + processMessage(e.data); + }); + port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. +}; + +function createClock( + callback, // called slightly before each cycle + duration, +) { + const interval = 0.1; + const overlap = interval / 2; + const precision = 10 ** 4; // used to round phase + const minLatency = 0.01; + let phase = 0; // next callback time + + const onTick = () => { + const t = getTime(precision); + const lookahead = t + interval + overlap; // the time window for this tick + if (phase === 0) { + phase = t + minLatency; + } + // callback as long as we're inside the lookahead + while (phase < lookahead) { + phase = Math.round(phase * precision) / precision; + phase >= t && callback({ phase, duration, time: t }); + phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? + phase += duration; // increment phase by duration + } + }; + let intervalID; + const start = () => { + clear(); // just in case start was called more than once + onTick(); + intervalID = setInterval(onTick, interval * 1000); + }; + const clear = () => intervalID !== undefined && clearInterval(intervalID); + const stop = () => { + phase = 0; + clear(); + }; + + return { start, stop }; +} diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js deleted file mode 100644 index 2357c0df..00000000 --- a/packages/core/cyclistworker.js +++ /dev/null @@ -1,154 +0,0 @@ -const allPorts = []; -let cps = 1; -let num_ticks_since_cps_change = 0; -let lastTick = 0; // absolute time when last tick (clock callback) happened -let lastBegin = 0; // query begin of last tick -let lastEnd = 0; // query end of last tick -let num_cycles_at_cps_change = 0; -let interval = 0.1; -let started = false; - -//incoming -//cps message -// {type: 'cpschange', payload: {cps}} - -//toggle -// {type: toggle, payload?: {started: boolean}} - -//sending -//{type: 'tick', payload: {begin, end, tickdeadline, cps, time }} -//{type: 'log', payload: {type, text}} - -const getTime = () => { - return performance.now() / 1000; -}; - -const sendMessage = (type, payload) => { - allPorts.forEach((port) => { - port.postMessage({ type, payload }); - }); -}; -const log = (text, type) => { - sendMessage('log', { text, type }); -}; - -const numClientsConnected = () => allPorts.length; - -const getCycle = () => { - const secondsSinceLastTick = getTime() - lastTick - clock.duration; - const cycle = lastBegin + secondsSinceLastTick * cps; - return cycle; -}; -// let prevtime = 0; -let clock = createClock( - getTime, - // called slightly before each cycle - (phase, duration, tick) => { - if (num_ticks_since_cps_change === 0) { - num_cycles_at_cps_change = lastEnd; - } - num_ticks_since_cps_change++; - // const now = Date.now(); - // console.log('interval', now - prevtime); - // prevtime = now; - try { - const time = getTime(); - const begin = lastEnd; - lastBegin = begin; - //convert ticks to cycles, so you can query the pattern for events - const eventLength = duration * cps; - const num_cycles_since_cps_change = num_ticks_since_cps_change * eventLength; - const end = num_cycles_at_cps_change + num_cycles_since_cps_change; - lastEnd = end; - const tickdeadline = phase - time; // time left until the phase is a whole number - lastTick = time + tickdeadline; - sendMessage('tick', { begin, end, tickdeadline, cps, cycle: getCycle() }); - } catch (e) { - log(`[cyclist] error: ${e.message}`, 'error'); - } - }, - interval, // duration of each cycle -); - -self.onconnect = function (e) { - // the incoming port - const port = e.ports[0]; - allPorts.push(port); - port.addEventListener('message', function (e) { - processMessage(e.data); - }); - port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. -}; - -const processMessage = (message) => { - const { type, payload } = message; - - switch (type) { - case 'cpschange': { - if (payload.cps !== cps) { - cps = payload.cps; - num_ticks_since_cps_change = 0; - } - break; - } - case 'toggle': { - if (payload.started && !started) { - started = true; - clock.start(); - //dont stop the clock if others are using it... - } else if (numClientsConnected() === 1) { - started = false; - clock.stop(); - } - break; - } - } -}; - -function createClock( - getTime, - callback, // called slightly before each cycle - duration = 0.05, // duration of each cycle - interval = 0.1, // interval between callbacks - overlap = 0.1, // overlap between callbacks -) { - let tick = 0; // counts callbacks - let phase = 0; // next callback time - let precision = 10 ** 4; // used to round phase - let minLatency = 0.01; - - const setDuration = (setter) => (duration = setter(duration)); - overlap = overlap || interval / 2; - const onTick = () => { - const t = getTime(); - const lookahead = t + interval + overlap; // the time window for this tick - if (phase === 0) { - phase = t + minLatency; - } - - // callback as long as we're inside the lookahead - while (phase < lookahead) { - phase = Math.round(phase * precision) / precision; - phase >= t && callback(phase, duration, tick); - phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? - phase += duration; // increment phase by duration - tick++; - } - }; - let intervalID; - const start = () => { - clear(); // just in case start was called more than once - onTick(); - intervalID = setInterval(onTick, interval * 1000); - }; - const clear = () => intervalID !== undefined && clearInterval(intervalID); - const pause = () => clear(); - const stop = () => { - tick = 0; - phase = 0; - clear(); - }; - const getPhase = () => phase; - - return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; -} diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 41a7d691..db58e61e 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -4,12 +4,10 @@ import { logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; -import { NeoCyclist } from './neocyclist.mjs'; export function repl({ - interval, defaultOutput, - onSchedulerError, + onEvalError, beforeEval, afterEval, @@ -38,28 +36,14 @@ export function repl({ onUpdateState?.(state); }; - // const scheduler = new Cyclist({ - // interval, - // onTrigger: getTrigger({ defaultOutput, getTime }), - // onError: onSchedulerError, - // getTime, - // onToggle: (started) => { - // updateState({ started }); - // onToggle?.(started); - // }, - // }); - - const scheduler = new NeoCyclist({ - // interval, + const scheduler = new Cyclist({ onTrigger: getTrigger({ defaultOutput, getTime }), - onError: onSchedulerError, - // latency: 0.22, + getTime, onToggle: (started) => { updateState({ started }); onToggle?.(started); }, }); - let pPatterns = {}; let allTransform; @@ -74,67 +58,12 @@ export function repl({ scheduler.setPattern(pattern, autostart); }; setTime(() => scheduler.now()); // TODO: refactor? - - const stop = () => scheduler.stop(); - const start = () => scheduler.start(); - const pause = () => scheduler.pause(); - const toggle = () => scheduler.toggle(); - const setCps = (cps) => scheduler.setCps(cps); - const setCpm = (cpm) => scheduler.setCps(cpm / 60); - const all = function (transform) { - allTransform = transform; - return silence; - }; - - // set pattern methods that use this repl via closure - const injectPatternMethods = () => { - Pattern.prototype.p = function (id) { - pPatterns[id] = this; - return this; - }; - Pattern.prototype.q = function (id) { - return silence; - }; - try { - for (let i = 1; i < 10; ++i) { - Object.defineProperty(Pattern.prototype, `d${i}`, { - get() { - return this.p(i); - }, - configurable: true, - }); - Object.defineProperty(Pattern.prototype, `p${i}`, { - get() { - return this.p(i); - }, - configurable: true, - }); - Pattern.prototype[`q${i}`] = silence; - } - } catch (err) { - console.warn('injectPatternMethods: error:', err); - } - const cpm = register('cpm', function (cpm, pat) { - return pat._fast(cpm / 60 / scheduler.cps); - }); - evalScope({ - all, - hush, - cpm, - setCps, - setcps: setCps, - setCpm, - setcpm: setCpm, - }); - }; - const evaluate = async (code, autostart = true, shouldHush = true) => { if (!code) { throw new Error('no code to evaluate'); } try { updateState({ code, pending: true }); - injectPatternMethods(); await beforeEval?.({ code }); shouldHush && hush(); let { pattern, meta } = await _evaluate(code, transpiler); @@ -162,11 +91,74 @@ export function repl({ afterEval?.({ code, pattern, meta }); return pattern; } catch (err) { + // console.warn(`[repl] eval error: ${err.message}`); logger(`[eval] error: ${err.message}`, 'error'); updateState({ evalError: err, pending: false }); onEvalError?.(err); } }; + const stop = () => scheduler.stop(); + const start = () => scheduler.start(); + const pause = () => scheduler.pause(); + const toggle = () => scheduler.toggle(); + const setCps = (cps) => scheduler.setCps(cps); + const setCpm = (cpm) => scheduler.setCps(cpm / 60); + + // the following functions use the cps value, which is why they are defined here.. + const loopAt = register('loopAt', (cycles, pat) => { + return pat.loopAtCps(cycles, scheduler.cps); + }); + + Pattern.prototype.p = function (id) { + pPatterns[id] = this; + return this; + }; + Pattern.prototype.q = function (id) { + return silence; + }; + + const all = function (transform) { + allTransform = transform; + return silence; + }; + try { + for (let i = 1; i < 10; ++i) { + Object.defineProperty(Pattern.prototype, `d${i}`, { + get() { + return this.p(i); + }, + }); + Object.defineProperty(Pattern.prototype, `p${i}`, { + get() { + return this.p(i); + }, + }); + Pattern.prototype[`q${i}`] = silence; + } + } catch (err) { + // already defined.. + } + + const fit = register('fit', (pat) => + pat.withHap((hap) => + hap.withValue((v) => ({ + ...v, + speed: scheduler.cps / hap.whole.duration, // overwrite speed completely? + unit: 'c', + })), + ), + ); + + evalScope({ + loopAt, + fit, + all, + hush, + setCps, + setcps: setCps, + setCpm, + setcpm: setCpm, + }); const setCode = (code) => updateState({ code }); return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } diff --git a/packages/core/zyklus.mjs b/packages/core/zyklus.mjs deleted file mode 100644 index 3d25b054..00000000 --- a/packages/core/zyklus.mjs +++ /dev/null @@ -1,49 +0,0 @@ -// will move to https://github.com/felixroos/zyklus -// TODO: started flag - -function createClock( - getTime, - callback, // called slightly before each cycle - duration = 0.05, // duration of each cycle - interval = 0.1, // interval between callbacks - overlap = 0.1, // overlap between callbacks -) { - let tick = 0; // counts callbacks - let phase = 0; // next callback time - let precision = 10 ** 4; // used to round phase - let minLatency = 0.01; - const setDuration = (setter) => (duration = setter(duration)); - overlap = overlap || interval / 2; - const onTick = () => { - const t = getTime(); - const lookahead = t + interval + overlap; // the time window for this tick - if (phase === 0) { - phase = t + minLatency; - } - // callback as long as we're inside the lookahead - while (phase < lookahead) { - phase = Math.round(phase * precision) / precision; - phase >= t && callback(phase, duration, tick); - phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? - phase += duration; // increment phase by duration - tick++; - } - }; - let intervalID; - const start = () => { - clear(); // just in case start was called more than once - onTick(); - intervalID = setInterval(onTick, interval * 1000); - }; - const clear = () => intervalID !== undefined && clearInterval(intervalID); - const pause = () => clear(); - const stop = () => { - tick = 0; - phase = 0; - clear(); - }; - const getPhase = () => phase; - // setCallback - return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; -} -export default createClock; From f5ac57f87b28354b58a63b7e7fb03971da159584 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 13:56:46 -0500 Subject: [PATCH 12/69] cleaning up --- packages/core/neocyclist.mjs | 87 ---------------------------------- packages/webaudio/webaudio.mjs | 7 +-- 2 files changed, 1 insertion(+), 93 deletions(-) delete mode 100644 packages/core/neocyclist.mjs diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs deleted file mode 100644 index 3f8d4a83..00000000 --- a/packages/core/neocyclist.mjs +++ /dev/null @@ -1,87 +0,0 @@ -import { logger } from './logger.mjs'; - -export class NeoCyclist { - constructor({ onTrigger, onToggle, latency = 0.1, onError }) { - this.started = false; - this.pattern; - this.onToggle = onToggle; - this.latency = latency; - this.worker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); - this.worker.port.start(); - this.cycle = 0; - this.cps = 1; - this.timeAtLastTick = 0; - this.worker.port.addEventListener('message', (message) => { - if (!this.started) { - return; - } - const { payload, type } = message.data; - - switch (type) { - case 'tick': { - const now = performance.now(); - // const interval = 0.1; - // const timeSinceLastMessage = now - this.timeAtLastTickMessage; - // const messageLag = (interval * 1000 - timeSinceLastMessage) / 1000; - - this.timeAtLastTickMessage = now; - let { begin, end, cps, tickdeadline, cycle } = payload; - this.cps = cps; - this.cycle = cycle + latency * cps; - - const haps = this.pattern.queryArc(begin, end); - haps.forEach((hap) => { - if (hap.part.begin.equals(hap.whole.begin)) { - const deadline = (hap.whole.begin - begin) / cps + tickdeadline + latency; - const duration = hap.duration / cps; - onTrigger?.(hap, deadline, duration, cps); - } - }); - break; - } - case 'log': { - const { type, text } = payload; - if (type == 'error') { - onError(text); - } else { - logger(text, type); - } - } - } - }); - } - sendMessage(type, payload) { - this.worker.port.postMessage({ type, payload }); - } - - now() { - const gap = ((performance.now() - this.timeAtLastTickMessage) / 1000) * this.cps; - return this.cycle + gap; - } - setCps(cps = 1) { - this.sendMessage('cpschange', { cps }); - } - setStarted(started) { - this.sendMessage('toggle', { started }); - this.started = started; - this.onToggle?.(started); - } - start() { - logger('[cyclist] start'); - this.setStarted(true); - } - stop() { - logger('[cyclist] stop'); - this.setStarted(false); - } - setPattern(pat, autostart = false) { - this.pattern = pat; - if (autostart && !this.started) { - this.start(); - } - } - log(begin, end, haps) { - const onsets = haps.filter((h) => h.hasOnset()); - console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); - } -} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 83dbc698..19dbb804 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -30,12 +30,7 @@ export function webaudioScheduler(options = {}) { ...options, }; const { defaultOutput, getTime } = options; - // return new strudel.Cyclist({ - // ...options, - // onTrigger: strudel.getTrigger({ defaultOutput, getTime }), - // }); - console.log('here'); - return new strudel.NeoCyclist({ + return new strudel.Cyclist({ ...options, onTrigger: strudel.getTrigger({ defaultOutput, getTime }), }); From fc87a6776ae9318e55695bb84341b1cc1cd0dc45 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 14:04:12 -0500 Subject: [PATCH 13/69] cleaning up --- packages/core/index.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/index.mjs b/packages/core/index.mjs index d7d2f759..d9386417 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -19,7 +19,6 @@ export * from './speak.mjs'; export * from './evaluate.mjs'; export * from './repl.mjs'; export * from './cyclist.mjs'; -export * from './neocyclist.mjs'; export * from './logger.mjs'; export * from './time.mjs'; export * from './draw.mjs'; From cc323d0d611b01f9a7de4728753bd29071de41e9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 14:09:19 -0500 Subject: [PATCH 14/69] fixed import --- packages/core/{clockworker.mjs => clockworker.js} | 0 packages/core/cyclist.mjs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/core/{clockworker.mjs => clockworker.js} (100%) diff --git a/packages/core/clockworker.mjs b/packages/core/clockworker.js similarity index 100% rename from packages/core/clockworker.mjs rename to packages/core/clockworker.js diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index f44b015e..706313a8 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -18,7 +18,7 @@ export class Cyclist { this.latency = 0.1; // fixed trigger time offset this.cycle = 0; - this.worker = new SharedWorker(new URL('./clockworker.mjs', import.meta.url)); + this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url)); this.worker.port.start(); let worker_time_dif = 0; // time difference between audio context clock and worker clock let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif From a8a055d32c87408be221fd3ef97bcf8d08affc15 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 14:25:16 -0500 Subject: [PATCH 15/69] fixing test --- packages/core/clockworker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 906e914f..e2f6b25d 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -79,7 +79,7 @@ const processMessage = (message) => { } }; -self.onconnect = function (e) { +onconnect = function (e) { // the incoming port const port = e.ports[0]; allPorts.push(port); From c761cd54b4c4f7e78e1a483fbb4951af8d262cab Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 14:32:42 -0500 Subject: [PATCH 16/69] try to fix test again: --- packages/core/clockworker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index e2f6b25d..89f0f9f1 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -79,7 +79,7 @@ const processMessage = (message) => { } }; -onconnect = function (e) { +this.onconnect = function (e) { // the incoming port const port = e.ports[0]; allPorts.push(port); From 0d3eaf7f9ae5402824f8588d347c9baf6532df03 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 17:06:28 -0500 Subject: [PATCH 17/69] add a broadcast channel for recieving messages from clock to ensure that they hit at the same time --- packages/core/clockworker.js | 18 +++++++++--------- packages/core/cyclist.mjs | 7 +++++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 89f0f9f1..7bd50c84 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -2,16 +2,16 @@ function getTime(precision) { const seconds = performance.now() / 1000; return Math.round(seconds * precision) / precision; } -const allPorts = []; + +let numPorts = 0; let num_cycles_at_cps_change = 0; let num_ticks_since_cps_change = 0; let cps = 0.5; const duration = 0.1; +const channel = new BroadcastChannel('strudeltick'); const sendMessage = (type, payload) => { - allPorts.forEach((port) => { - port.postMessage({ type, payload }); - }); + channel.postMessage({ type, payload }); }; const sendTick = ({ phase, duration, time }) => { @@ -36,9 +36,10 @@ const startClock = () => { clock.start(); started = true; }; -const stopClock = () => { +const stopClock = async () => { + console.log(numPorts); //dont stop the clock if mutliple instances are using it... - if (!started || numClientsConnected() > 1) { + if (!started || numPorts !== 1) { return; } clock.stop(); @@ -51,7 +52,6 @@ const setCycle = (cycle) => { num_cycles_at_cps_change = cycle; }; -const numClientsConnected = () => allPorts.length; const processMessage = (message) => { const { type, payload } = message; @@ -79,10 +79,10 @@ const processMessage = (message) => { } }; -this.onconnect = function (e) { +self.onconnect = function (e) { // the incoming port const port = e.ports[0]; - allPorts.push(port); + numPorts = numPorts + 1; port.addEventListener('message', function (e) { processMessage(e.data); }); diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 706313a8..84612f0d 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -20,6 +20,8 @@ export class Cyclist { this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url)); this.worker.port.start(); + + this.channel = new BroadcastChannel('strudeltick'); let worker_time_dif = 0; // time difference between audio context clock and worker clock let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif const maxWeight = 400; @@ -52,6 +54,7 @@ export class Cyclist { const tickCallback = (payload) => { const workertime = payload.time; const time = this.getTime(); + const { duration, phase, num_ticks_since_cps_change, num_cycles_at_cps_change, cps } = payload; setTimeReference(time, workertime); this.cps = cps; @@ -90,7 +93,7 @@ export class Cyclist { }; // receive messages from worker clock and process them - this.worker.port.addEventListener('message', (message) => { + this.channel.onmessage = (message) => { if (!this.started) { return; } @@ -101,7 +104,7 @@ export class Cyclist { tickCallback(payload); } } - }); + }; } sendMessage(type, payload) { this.worker.port.postMessage({ type, payload }); From cea33783a25eaa9c73b14bb0ecb35f45696b5703 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 17:10:43 -0500 Subject: [PATCH 18/69] remove console statement --- packages/core/clockworker.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 7bd50c84..eb82ece6 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -37,7 +37,6 @@ const startClock = () => { started = true; }; const stopClock = async () => { - console.log(numPorts); //dont stop the clock if mutliple instances are using it... if (!started || numPorts !== 1) { return; @@ -79,7 +78,7 @@ const processMessage = (message) => { } }; -self.onconnect = function (e) { +this.onconnect = function (e) { // the incoming port const port = e.ports[0]; numPorts = numPorts + 1; From 0baa8383f20ece63e53d1622a3b3529b32ba268a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 4 Feb 2024 23:32:28 +0100 Subject: [PATCH 19/69] fix: test --- packages/core/cyclist.mjs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 84612f0d..cf08b328 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -227,15 +227,17 @@ const processMessage = (message) => { } }; -self.onconnect = function (e) { - // the incoming port - const port = e.ports[0]; - allPorts.push(port); - port.addEventListener('message', function (e) { - processMessage(e.data); - }); - port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. -}; +if (typeof self !== 'undefined') { + self.onconnect = function (e) { + // the incoming port + const port = e.ports[0]; + allPorts.push(port); + port.addEventListener('message', function (e) { + processMessage(e.data); + }); + port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. + }; +} function createClock( callback, // called slightly before each cycle From 5a3272fe2906dbd282d675738019341fc25bb125 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 17:35:02 -0500 Subject: [PATCH 20/69] fix repl merge --- packages/core/repl.mjs | 119 +++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 64 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index db58e61e..8cad7141 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -7,7 +7,6 @@ import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; export function repl({ defaultOutput, - onEvalError, beforeEval, afterEval, @@ -58,12 +57,67 @@ export function repl({ scheduler.setPattern(pattern, autostart); }; setTime(() => scheduler.now()); // TODO: refactor? + + const stop = () => scheduler.stop(); + const start = () => scheduler.start(); + const pause = () => scheduler.pause(); + const toggle = () => scheduler.toggle(); + const setCps = (cps) => scheduler.setCps(cps); + const setCpm = (cpm) => scheduler.setCps(cpm / 60); + const all = function (transform) { + allTransform = transform; + return silence; + }; + + // set pattern methods that use this repl via closure + const injectPatternMethods = () => { + Pattern.prototype.p = function (id) { + pPatterns[id] = this; + return this; + }; + Pattern.prototype.q = function (id) { + return silence; + }; + try { + for (let i = 1; i < 10; ++i) { + Object.defineProperty(Pattern.prototype, `d${i}`, { + get() { + return this.p(i); + }, + configurable: true, + }); + Object.defineProperty(Pattern.prototype, `p${i}`, { + get() { + return this.p(i); + }, + configurable: true, + }); + Pattern.prototype[`q${i}`] = silence; + } + } catch (err) { + console.warn('injectPatternMethods: error:', err); + } + const cpm = register('cpm', function (cpm, pat) { + return pat._fast(cpm / 60 / scheduler.cps); + }); + evalScope({ + all, + hush, + cpm, + setCps, + setcps: setCps, + setCpm, + setcpm: setCpm, + }); + }; + const evaluate = async (code, autostart = true, shouldHush = true) => { if (!code) { throw new Error('no code to evaluate'); } try { updateState({ code, pending: true }); + injectPatternMethods(); await beforeEval?.({ code }); shouldHush && hush(); let { pattern, meta } = await _evaluate(code, transpiler); @@ -91,74 +145,11 @@ export function repl({ afterEval?.({ code, pattern, meta }); return pattern; } catch (err) { - // console.warn(`[repl] eval error: ${err.message}`); logger(`[eval] error: ${err.message}`, 'error'); updateState({ evalError: err, pending: false }); onEvalError?.(err); } }; - const stop = () => scheduler.stop(); - const start = () => scheduler.start(); - const pause = () => scheduler.pause(); - const toggle = () => scheduler.toggle(); - const setCps = (cps) => scheduler.setCps(cps); - const setCpm = (cpm) => scheduler.setCps(cpm / 60); - - // the following functions use the cps value, which is why they are defined here.. - const loopAt = register('loopAt', (cycles, pat) => { - return pat.loopAtCps(cycles, scheduler.cps); - }); - - Pattern.prototype.p = function (id) { - pPatterns[id] = this; - return this; - }; - Pattern.prototype.q = function (id) { - return silence; - }; - - const all = function (transform) { - allTransform = transform; - return silence; - }; - try { - for (let i = 1; i < 10; ++i) { - Object.defineProperty(Pattern.prototype, `d${i}`, { - get() { - return this.p(i); - }, - }); - Object.defineProperty(Pattern.prototype, `p${i}`, { - get() { - return this.p(i); - }, - }); - Pattern.prototype[`q${i}`] = silence; - } - } catch (err) { - // already defined.. - } - - const fit = register('fit', (pat) => - pat.withHap((hap) => - hap.withValue((v) => ({ - ...v, - speed: scheduler.cps / hap.whole.duration, // overwrite speed completely? - unit: 'c', - })), - ), - ); - - evalScope({ - loopAt, - fit, - all, - hush, - setCps, - setcps: setCps, - setCpm, - setcpm: setCpm, - }); const setCode = (code) => updateState({ code }); return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } From b37c560ea52eabffb872872825b0dd7efada4f4e Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 8 Feb 2024 00:20:40 -0500 Subject: [PATCH 21/69] importScripts --- packages/core/clockworker.js | 81 +++++++++++++++++++----------------- packages/core/zyklus.js | 48 +++++++++++++++++++++ 2 files changed, 90 insertions(+), 39 deletions(-) create mode 100644 packages/core/zyklus.js diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index eb82ece6..d9e9ae83 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -1,4 +1,7 @@ -function getTime(precision) { +importScripts('./zyklus.js'); + +function getTime() { + const precision = 10 ** 4; const seconds = performance.now() / 1000; return Math.round(seconds * precision) / precision; } @@ -14,7 +17,7 @@ const sendMessage = (type, payload) => { channel.postMessage({ type, payload }); }; -const sendTick = ({ phase, duration, time }) => { +const sendTick = (phase, duration, tick, time) => { sendMessage('tick', { phase, duration, @@ -26,7 +29,7 @@ const sendTick = ({ phase, duration, time }) => { num_ticks_since_cps_change++; }; -const clock = createClock(sendTick, duration); +const clock = this.createClock(getTime, sendTick, duration); let started = false; const startClock = () => { @@ -88,41 +91,41 @@ this.onconnect = function (e) { port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; -function createClock( - callback, // called slightly before each cycle - duration, -) { - const interval = 0.1; - const overlap = interval / 2; - const precision = 10 ** 4; // used to round phase - const minLatency = 0.01; - let phase = 0; // next callback time +// function createClock( +// callback, // called slightly before each cycle +// duration, +// ) { +// const interval = 0.1; +// const overlap = interval / 2; +// const precision = 10 ** 4; // used to round phase +// const minLatency = 0.01; +// let phase = 0; // next callback time - const onTick = () => { - const t = getTime(precision); - const lookahead = t + interval + overlap; // the time window for this tick - if (phase === 0) { - phase = t + minLatency; - } - // callback as long as we're inside the lookahead - while (phase < lookahead) { - phase = Math.round(phase * precision) / precision; - phase >= t && callback({ phase, duration, time: t }); - phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? - phase += duration; // increment phase by duration - } - }; - let intervalID; - const start = () => { - clear(); // just in case start was called more than once - onTick(); - intervalID = setInterval(onTick, interval * 1000); - }; - const clear = () => intervalID !== undefined && clearInterval(intervalID); - const stop = () => { - phase = 0; - clear(); - }; +// const onTick = () => { +// const t = getTime(precision); +// const lookahead = t + interval + overlap; // the time window for this tick +// if (phase === 0) { +// phase = t + minLatency; +// } +// // callback as long as we're inside the lookahead +// while (phase < lookahead) { +// phase = Math.round(phase * precision) / precision; +// phase >= t && callback({ phase, duration, time: t }); +// phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? +// phase += duration; // increment phase by duration +// } +// }; +// let intervalID; +// const start = () => { +// clear(); // just in case start was called more than once +// onTick(); +// intervalID = setInterval(onTick, interval * 1000); +// }; +// const clear = () => intervalID !== undefined && clearInterval(intervalID); +// const stop = () => { +// phase = 0; +// clear(); +// }; - return { start, stop }; -} +// return { start, stop }; +// } diff --git a/packages/core/zyklus.js b/packages/core/zyklus.js new file mode 100644 index 00000000..8d4d43b5 --- /dev/null +++ b/packages/core/zyklus.js @@ -0,0 +1,48 @@ +// will move to https://github.com/felixroos/zyklus +// TODO: started flag + +this.createClock = ( + getTime, + callback, // called slightly before each cycle + duration = 0.05, // duration of each cycle + interval = 0.1, // interval between callbacks + overlap = 0.1, // overlap between callbacks +) => { + let tick = 0; // counts callbacks + let phase = 0; // next callback time + let precision = 10 ** 4; // used to round phase + let minLatency = 0.01; + const setDuration = (setter) => (duration = setter(duration)); + overlap = overlap || interval / 2; + const onTick = () => { + const t = getTime(); + const lookahead = t + interval + overlap; // the time window for this tick + if (phase === 0) { + phase = t + minLatency; + } + // callback as long as we're inside the lookahead + while (phase < lookahead) { + phase = Math.round(phase * precision) / precision; + phase >= t && callback(phase, duration, tick, t); + phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? + phase += duration; // increment phase by duration + tick++; + } + }; + let intervalID; + const start = () => { + clear(); // just in case start was called more than once + onTick(); + intervalID = setInterval(onTick, interval * 1000); + }; + const clear = () => intervalID !== undefined && clearInterval(intervalID); + const pause = () => clear(); + const stop = () => { + tick = 0; + phase = 0; + clear(); + }; + const getPhase = () => phase; + // setCallback + return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; +}; From 15df20d22d14ae006a2701a397ea15673586bb21 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 8 Feb 2024 00:37:16 -0500 Subject: [PATCH 22/69] cleanup --- packages/core/clockworker.js | 40 +----------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index d9e9ae83..2b813769 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -29,6 +29,7 @@ const sendTick = (phase, duration, tick, time) => { num_ticks_since_cps_change++; }; +//create clock method from zyklus const clock = this.createClock(getTime, sendTick, duration); let started = false; @@ -90,42 +91,3 @@ this.onconnect = function (e) { }); port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; - -// function createClock( -// callback, // called slightly before each cycle -// duration, -// ) { -// const interval = 0.1; -// const overlap = interval / 2; -// const precision = 10 ** 4; // used to round phase -// const minLatency = 0.01; -// let phase = 0; // next callback time - -// const onTick = () => { -// const t = getTime(precision); -// const lookahead = t + interval + overlap; // the time window for this tick -// if (phase === 0) { -// phase = t + minLatency; -// } -// // callback as long as we're inside the lookahead -// while (phase < lookahead) { -// phase = Math.round(phase * precision) / precision; -// phase >= t && callback({ phase, duration, time: t }); -// phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? -// phase += duration; // increment phase by duration -// } -// }; -// let intervalID; -// const start = () => { -// clear(); // just in case start was called more than once -// onTick(); -// intervalID = setInterval(onTick, interval * 1000); -// }; -// const clear = () => intervalID !== undefined && clearInterval(intervalID); -// const stop = () => { -// phase = 0; -// clear(); -// }; - -// return { start, stop }; -// } From 751a825616d79314ba1a9bb1a8499c3417c641ea Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 19 Feb 2024 23:54:15 -0500 Subject: [PATCH 23/69] fix test --- packages/core/clockworker.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 2b813769..7ca04528 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line no-undef importScripts('./zyklus.js'); function getTime() { From e5570a601762fcb898b0a422a0266f06adf2181b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 21 Feb 2024 23:42:19 -0500 Subject: [PATCH 24/69] restore old cyclist --- packages/core/cyclist.mjs | 305 ++++++++--------------------------- packages/core/neocyclist.mjs | 147 +++++++++++++++++ packages/core/repl.mjs | 4 +- packages/core/zyklus.js | 92 +++++------ 4 files changed, 264 insertions(+), 284 deletions(-) create mode 100644 packages/core/neocyclist.mjs diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index cf08b328..f024c464 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -1,136 +1,93 @@ /* -cyclist.mjs - recieves clock pulses from clockworker, and schedules the next events +cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +import * as createClock from './zyklus.js'; import { logger } from './logger.mjs'; export class Cyclist { - constructor({ onTrigger, onToggle, getTime }) { + constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) { this.started = false; this.cps = 0.5; + this.num_ticks_since_cps_change = 0; this.lastTick = 0; // absolute time when last tick (clock callback) happened + this.lastBegin = 0; // query begin of last tick + this.lastEnd = 0; // query end of last tick this.getTime = getTime; // get absolute time - this.num_cycles_at_cps_change = 0; this.onToggle = onToggle; - this.latency = 0.1; // fixed trigger time offset - this.cycle = 0; - - this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url)); - this.worker.port.start(); - - this.channel = new BroadcastChannel('strudeltick'); - let worker_time_dif = 0; // time difference between audio context clock and worker clock - let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif - const maxWeight = 400; - const precision = 10 ** 3; //round off time diff to prevent accumulating outliers - - // the clock of the worker and the audio context clock can drift apart over time - // aditionally, the message time of the worker pinging the callback to process haps can be inconsistent. - // we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock - // in order to schedule events consistently. - const setTimeReference = (time, workertime) => { - const time_dif = workertime - time; - if (worker_time_dif === 0) { - worker_time_dif = time_dif; - } else { - const w = 1; //weight of new time diff; - const new_dif = Math.round(((worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision; - - if (new_dif != worker_time_dif) { - // reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens - weight = 4; + this.latency = latency; // fixed trigger time offset + this.clock = createClock( + getTime, + // called slightly before each cycle + (phase, duration, tick) => { + if (tick === 0) { + this.origin = phase; } - worker_time_dif = new_dif; - } - }; - - const getTickDeadline = (phase, time) => { - return phase - time - worker_time_dif; - }; - - const tickCallback = (payload) => { - const workertime = payload.time; - const time = this.getTime(); - - const { duration, phase, num_ticks_since_cps_change, num_cycles_at_cps_change, cps } = payload; - setTimeReference(time, workertime); - this.cps = cps; - - //calculate begin and end - const eventLength = duration * cps; - const num_cycles_since_cps_change = num_ticks_since_cps_change * eventLength; - const begin = num_cycles_at_cps_change + num_cycles_since_cps_change; - const tickdeadline = getTickDeadline(phase, time); - const end = begin + eventLength; - - //calculate current cycle - const lastTick = time + tickdeadline; - const secondsSinceLastTick = time - lastTick - duration; - this.cycle = begin + secondsSinceLastTick * cps; - - //set the weight of average time diff and processs haps - weight = Math.min(weight + 1, maxWeight); - processHaps(begin, end, tickdeadline); - this.time_at_last_tick_message = this.getTime(); - }; - - const processHaps = (begin, end, tickdeadline) => { - if (this.started === false) { - return; - } - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); - - haps.forEach((hap) => { - if (hap.part.begin.equals(hap.whole.begin)) { - const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + this.latency; - const duration = hap.duration / this.cps; - onTrigger?.(hap, deadline, duration, this.cps); + if (this.num_ticks_since_cps_change === 0) { + this.num_cycles_at_cps_change = this.lastEnd; } - }); - }; + this.num_ticks_since_cps_change++; + try { + const time = getTime(); + const begin = this.lastEnd; + this.lastBegin = begin; - // receive messages from worker clock and process them - this.channel.onmessage = (message) => { - if (!this.started) { - return; - } - const { payload, type } = message.data; + //convert ticks to cycles, so you can query the pattern for events + const eventLength = duration * this.cps; + const end = this.num_cycles_at_cps_change + this.num_ticks_since_cps_change * eventLength; + this.lastEnd = end; - switch (type) { - case 'tick': { - tickCallback(payload); + // query the pattern for events + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + + const tickdeadline = phase - time; // time left until the phase is a whole number + this.lastTick = time + tickdeadline; + + haps.forEach((hap) => { + if (hap.part.begin.equals(hap.whole.begin)) { + const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency; + const duration = hap.duration / this.cps; + onTrigger?.(hap, deadline, duration, this.cps); + } + }); + } catch (e) { + logger(`[cyclist] error: ${e.message}`); + onError?.(e); } - } - }; + }, + interval, // duration of each cycle + ); } - sendMessage(type, payload) { - this.worker.port.postMessage({ type, payload }); - } - now() { - const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps; - return this.cycle + gap; + const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; + return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; } - setCps(cps = 1) { - this.sendMessage('cpschange', { cps }); - } - setCycle(cycle) { - this.sendMessage('setcycle', { cycle }); - } - setStarted(started) { - this.sendMessage('toggle', { started }); - this.started = started; - this.onToggle?.(started); + setStarted(v) { + this.started = v; + this.onToggle?.(v); } start() { + this.num_ticks_since_cps_change = 0; + this.num_cycles_at_cps_change = 0; + if (!this.pattern) { + throw new Error('Scheduler: no pattern set! call .setPattern first.'); + } logger('[cyclist] start'); + this.clock.start(); this.setStarted(true); } + pause() { + logger('[cyclist] pause'); + this.clock.pause(); + this.setStarted(false); + } stop() { logger('[cyclist] stop'); + this.clock.stop(); + this.lastEnd = 0; this.setStarted(false); } setPattern(pat, autostart = false) { @@ -139,141 +96,15 @@ export class Cyclist { this.start(); } } - + setCps(cps = 0.5) { + if (this.cps === cps) { + return; + } + this.cps = cps; + this.num_ticks_since_cps_change = 0; + } log(begin, end, haps) { const onsets = haps.filter((h) => h.hasOnset()); console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); } } - -function getTime(precision) { - const seconds = performance.now() / 1000; - return Math.round(seconds * precision) / precision; -} -const allPorts = []; -let num_cycles_at_cps_change = 0; -let num_ticks_since_cps_change = 0; -let cps = 0.5; -const duration = 0.1; - -const sendMessage = (type, payload) => { - allPorts.forEach((port) => { - port.postMessage({ type, payload }); - }); -}; - -const sendTick = ({ phase, duration, time }) => { - sendMessage('tick', { - phase, - duration, - time, - cps, - num_cycles_at_cps_change, - num_ticks_since_cps_change, - }); - num_ticks_since_cps_change++; -}; - -const clock = createClock(sendTick, duration); -let started = false; - -const startClock = () => { - if (started) { - return; - } - clock.start(); - started = true; -}; -const stopClock = () => { - //dont stop the clock if mutliple instances are using it... - if (!started || numClientsConnected() > 1) { - return; - } - clock.stop(); - setCycle(0); - started = false; -}; - -const setCycle = (cycle) => { - num_ticks_since_cps_change = 0; - num_cycles_at_cps_change = cycle; -}; - -const numClientsConnected = () => allPorts.length; -const processMessage = (message) => { - const { type, payload } = message; - - switch (type) { - case 'cpschange': { - if (payload.cps !== cps) { - num_cycles_at_cps_change = num_cycles_at_cps_change + num_ticks_since_cps_change * duration * cps; - cps = payload.cps; - num_ticks_since_cps_change = 0; - } - break; - } - case 'setcycle': { - setCycle(payload.cycle); - break; - } - case 'toggle': { - if (payload.started) { - startClock(); - } else { - stopClock(); - } - break; - } - } -}; - -if (typeof self !== 'undefined') { - self.onconnect = function (e) { - // the incoming port - const port = e.ports[0]; - allPorts.push(port); - port.addEventListener('message', function (e) { - processMessage(e.data); - }); - port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. - }; -} - -function createClock( - callback, // called slightly before each cycle - duration, -) { - const interval = 0.1; - const overlap = interval / 2; - const precision = 10 ** 4; // used to round phase - const minLatency = 0.01; - let phase = 0; // next callback time - - const onTick = () => { - const t = getTime(precision); - const lookahead = t + interval + overlap; // the time window for this tick - if (phase === 0) { - phase = t + minLatency; - } - // callback as long as we're inside the lookahead - while (phase < lookahead) { - phase = Math.round(phase * precision) / precision; - phase >= t && callback({ phase, duration, time: t }); - phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? - phase += duration; // increment phase by duration - } - }; - let intervalID; - const start = () => { - clear(); // just in case start was called more than once - onTick(); - intervalID = setInterval(onTick, interval * 1000); - }; - const clear = () => intervalID !== undefined && clearInterval(intervalID); - const stop = () => { - phase = 0; - clear(); - }; - - return { start, stop }; -} diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs new file mode 100644 index 00000000..ada66cf4 --- /dev/null +++ b/packages/core/neocyclist.mjs @@ -0,0 +1,147 @@ +/* +neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances. +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { logger } from './logger.mjs'; + +export class NeoCyclist { + constructor({ onTrigger, onToggle, getTime }) { + this.started = false; + this.cps = 0.5; + this.lastTick = 0; // absolute time when last tick (clock callback) happened + this.getTime = getTime; // get absolute time + + this.num_cycles_at_cps_change = 0; + this.onToggle = onToggle; + this.latency = 0.1; // fixed trigger time offset + this.cycle = 0; + + this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url)); + this.worker.port.start(); + + this.channel = new BroadcastChannel('strudeltick'); + let worker_time_dif = 0; // time difference between audio context clock and worker clock + let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif + const maxWeight = 400; + const precision = 10 ** 3; //round off time diff to prevent accumulating outliers + + // the clock of the worker and the audio context clock can drift apart over time + // aditionally, the message time of the worker pinging the callback to process haps can be inconsistent. + // we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock + // in order to schedule events consistently. + const setTimeReference = (time, workertime) => { + const time_dif = workertime - time; + if (worker_time_dif === 0) { + worker_time_dif = time_dif; + } else { + const w = 1; //weight of new time diff; + const new_dif = Math.round(((worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision; + + if (new_dif != worker_time_dif) { + // reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens + weight = 4; + } + worker_time_dif = new_dif; + } + }; + + const getTickDeadline = (phase, time) => { + return phase - time - worker_time_dif; + }; + + const tickCallback = (payload) => { + const workertime = payload.time; + const time = this.getTime(); + + const { duration, phase, num_ticks_since_cps_change, num_cycles_at_cps_change, cps } = payload; + setTimeReference(time, workertime); + this.cps = cps; + + //calculate begin and end + const eventLength = duration * cps; + const num_cycles_since_cps_change = num_ticks_since_cps_change * eventLength; + const begin = num_cycles_at_cps_change + num_cycles_since_cps_change; + const tickdeadline = getTickDeadline(phase, time); + const end = begin + eventLength; + + //calculate current cycle + const lastTick = time + tickdeadline; + const secondsSinceLastTick = time - lastTick - duration; + this.cycle = begin + secondsSinceLastTick * cps; + + //set the weight of average time diff and processs haps + weight = Math.min(weight + 1, maxWeight); + processHaps(begin, end, tickdeadline); + this.time_at_last_tick_message = this.getTime(); + }; + + const processHaps = (begin, end, tickdeadline) => { + if (this.started === false) { + return; + } + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + + haps.forEach((hap) => { + if (hap.part.begin.equals(hap.whole.begin)) { + const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + this.latency; + const duration = hap.duration / this.cps; + onTrigger?.(hap, deadline, duration, this.cps); + } + }); + }; + + // receive messages from worker clock and process them + this.channel.onmessage = (message) => { + if (!this.started) { + return; + } + const { payload, type } = message.data; + + switch (type) { + case 'tick': { + tickCallback(payload); + } + } + }; + } + sendMessage(type, payload) { + this.worker.port.postMessage({ type, payload }); + } + + now() { + const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps; + return this.cycle + gap; + } + setCps(cps = 1) { + this.sendMessage('cpschange', { cps }); + } + setCycle(cycle) { + this.sendMessage('setcycle', { cycle }); + } + setStarted(started) { + this.sendMessage('toggle', { started }); + this.started = started; + this.onToggle?.(started); + } + start() { + logger('[cyclist] start'); + this.setStarted(true); + } + stop() { + logger('[cyclist] stop'); + this.setStarted(false); + } + setPattern(pat, autostart = false) { + this.pattern = pat; + if (autostart && !this.started) { + this.start(); + } + } + + log(begin, end, haps) { + const onsets = haps.filter((h) => h.hasOnset()); + console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); + } +} diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 8cad7141..0ec106af 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -1,4 +1,4 @@ -import { Cyclist } from './cyclist.mjs'; +import { NeoCyclist } from './neocyclist.mjs'; import { evaluate as _evaluate } from './evaluate.mjs'; import { logger } from './logger.mjs'; import { setTime } from './time.mjs'; @@ -35,7 +35,7 @@ export function repl({ onUpdateState?.(state); }; - const scheduler = new Cyclist({ + const scheduler = new NeoCyclist({ onTrigger: getTrigger({ defaultOutput, getTime }), getTime, onToggle: (started) => { diff --git a/packages/core/zyklus.js b/packages/core/zyklus.js index 8d4d43b5..7a9c7fa5 100644 --- a/packages/core/zyklus.js +++ b/packages/core/zyklus.js @@ -1,48 +1,50 @@ // will move to https://github.com/felixroos/zyklus // TODO: started flag - -this.createClock = ( - getTime, - callback, // called slightly before each cycle - duration = 0.05, // duration of each cycle - interval = 0.1, // interval between callbacks - overlap = 0.1, // overlap between callbacks -) => { - let tick = 0; // counts callbacks - let phase = 0; // next callback time - let precision = 10 ** 4; // used to round phase - let minLatency = 0.01; - const setDuration = (setter) => (duration = setter(duration)); - overlap = overlap || interval / 2; - const onTick = () => { - const t = getTime(); - const lookahead = t + interval + overlap; // the time window for this tick - if (phase === 0) { - phase = t + minLatency; - } - // callback as long as we're inside the lookahead - while (phase < lookahead) { - phase = Math.round(phase * precision) / precision; - phase >= t && callback(phase, duration, tick, t); - phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? - phase += duration; // increment phase by duration - tick++; - } +//TODO: fix tests not understanding "self" +if (typeof self !== 'undefined') { + self.createClock = ( + getTime, + callback, // called slightly before each cycle + duration = 0.05, // duration of each cycle + interval = 0.1, // interval between callbacks + overlap = 0.1, // overlap between callbacks + ) => { + let tick = 0; // counts callbacks + let phase = 0; // next callback time + let precision = 10 ** 4; // used to round phase + let minLatency = 0.01; + const setDuration = (setter) => (duration = setter(duration)); + overlap = overlap || interval / 2; + const onTick = () => { + const t = getTime(); + const lookahead = t + interval + overlap; // the time window for this tick + if (phase === 0) { + phase = t + minLatency; + } + // callback as long as we're inside the lookahead + while (phase < lookahead) { + phase = Math.round(phase * precision) / precision; + phase >= t && callback(phase, duration, tick, t); + phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? + phase += duration; // increment phase by duration + tick++; + } + }; + let intervalID; + const start = () => { + clear(); // just in case start was called more than once + onTick(); + intervalID = setInterval(onTick, interval * 1000); + }; + const clear = () => intervalID !== undefined && clearInterval(intervalID); + const pause = () => clear(); + const stop = () => { + tick = 0; + phase = 0; + clear(); + }; + const getPhase = () => phase; + // setCallback + return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; }; - let intervalID; - const start = () => { - clear(); // just in case start was called more than once - onTick(); - intervalID = setInterval(onTick, interval * 1000); - }; - const clear = () => intervalID !== undefined && clearInterval(intervalID); - const pause = () => clear(); - const stop = () => { - tick = 0; - phase = 0; - clear(); - }; - const getPhase = () => phase; - // setCallback - return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; -}; +} From d544bf466500727233bea8192a24d9cfc6fbed1f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 22 Feb 2024 00:16:36 -0500 Subject: [PATCH 25/69] seperated service worker zyklus and cyclist zyklus because of import constraints on service workers --- packages/core/clockworker.js | 2 +- packages/core/cyclist.mjs | 2 +- packages/core/neozyklus.js | 46 ++++++++++++++++++ packages/core/zyklus.js | 91 ++++++++++++++++++------------------ 4 files changed, 93 insertions(+), 48 deletions(-) create mode 100644 packages/core/neozyklus.js diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 7ca04528..db9ef123 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -1,5 +1,5 @@ // eslint-disable-next-line no-undef -importScripts('./zyklus.js'); +importScripts('./neozyklus.js'); function getTime() { const precision = 10 ** 4; diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index f024c464..a8ccd30f 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import * as createClock from './zyklus.js'; +import createClock from './zyklus'; import { logger } from './logger.mjs'; export class Cyclist { diff --git a/packages/core/neozyklus.js b/packages/core/neozyklus.js new file mode 100644 index 00000000..9ec4e775 --- /dev/null +++ b/packages/core/neozyklus.js @@ -0,0 +1,46 @@ +// used to consistently schedule events, for use in a service worker - see +this.createClock = ( + getTime, + callback, // called slightly before each cycle + duration = 0.05, // duration of each cycle + interval = 0.1, // interval between callbacks + overlap = 0.1, // overlap between callbacks +) => { + let tick = 0; // counts callbacks + let phase = 0; // next callback time + let precision = 10 ** 4; // used to round phase + let minLatency = 0.01; + const setDuration = (setter) => (duration = setter(duration)); + overlap = overlap || interval / 2; + const onTick = () => { + const t = getTime(); + const lookahead = t + interval + overlap; // the time window for this tick + if (phase === 0) { + phase = t + minLatency; + } + // callback as long as we're inside the lookahead + while (phase < lookahead) { + phase = Math.round(phase * precision) / precision; + phase >= t && callback(phase, duration, tick, t); + phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? + phase += duration; // increment phase by duration + tick++; + } + }; + let intervalID; + const start = () => { + clear(); // just in case start was called more than once + onTick(); + intervalID = setInterval(onTick, interval * 1000); + }; + const clear = () => intervalID !== undefined && clearInterval(intervalID); + const pause = () => clear(); + const stop = () => { + tick = 0; + phase = 0; + clear(); + }; + const getPhase = () => phase; + // setCallback + return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; +}; diff --git a/packages/core/zyklus.js b/packages/core/zyklus.js index 7a9c7fa5..3d25b054 100644 --- a/packages/core/zyklus.js +++ b/packages/core/zyklus.js @@ -1,50 +1,49 @@ // will move to https://github.com/felixroos/zyklus // TODO: started flag -//TODO: fix tests not understanding "self" -if (typeof self !== 'undefined') { - self.createClock = ( - getTime, - callback, // called slightly before each cycle - duration = 0.05, // duration of each cycle - interval = 0.1, // interval between callbacks - overlap = 0.1, // overlap between callbacks - ) => { - let tick = 0; // counts callbacks - let phase = 0; // next callback time - let precision = 10 ** 4; // used to round phase - let minLatency = 0.01; - const setDuration = (setter) => (duration = setter(duration)); - overlap = overlap || interval / 2; - const onTick = () => { - const t = getTime(); - const lookahead = t + interval + overlap; // the time window for this tick - if (phase === 0) { - phase = t + minLatency; - } - // callback as long as we're inside the lookahead - while (phase < lookahead) { - phase = Math.round(phase * precision) / precision; - phase >= t && callback(phase, duration, tick, t); - phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? - phase += duration; // increment phase by duration - tick++; - } - }; - let intervalID; - const start = () => { - clear(); // just in case start was called more than once - onTick(); - intervalID = setInterval(onTick, interval * 1000); - }; - const clear = () => intervalID !== undefined && clearInterval(intervalID); - const pause = () => clear(); - const stop = () => { - tick = 0; - phase = 0; - clear(); - }; - const getPhase = () => phase; - // setCallback - return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; + +function createClock( + getTime, + callback, // called slightly before each cycle + duration = 0.05, // duration of each cycle + interval = 0.1, // interval between callbacks + overlap = 0.1, // overlap between callbacks +) { + let tick = 0; // counts callbacks + let phase = 0; // next callback time + let precision = 10 ** 4; // used to round phase + let minLatency = 0.01; + const setDuration = (setter) => (duration = setter(duration)); + overlap = overlap || interval / 2; + const onTick = () => { + const t = getTime(); + const lookahead = t + interval + overlap; // the time window for this tick + if (phase === 0) { + phase = t + minLatency; + } + // callback as long as we're inside the lookahead + while (phase < lookahead) { + phase = Math.round(phase * precision) / precision; + phase >= t && callback(phase, duration, tick); + phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? + phase += duration; // increment phase by duration + tick++; + } }; + let intervalID; + const start = () => { + clear(); // just in case start was called more than once + onTick(); + intervalID = setInterval(onTick, interval * 1000); + }; + const clear = () => intervalID !== undefined && clearInterval(intervalID); + const pause = () => clear(); + const stop = () => { + tick = 0; + phase = 0; + clear(); + }; + const getPhase = () => phase; + // setCallback + return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; } +export default createClock; From e54b2fb207a16deab4d2da57addec03cc0a9a364 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 23 Feb 2024 20:13:08 -0500 Subject: [PATCH 26/69] lower weighted average --- packages/core/neocyclist.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index ada66cf4..3cfb8d64 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -24,7 +24,7 @@ export class NeoCyclist { this.channel = new BroadcastChannel('strudeltick'); let worker_time_dif = 0; // time difference between audio context clock and worker clock let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif - const maxWeight = 400; + const maxWeight = 20; const precision = 10 ** 3; //round off time diff to prevent accumulating outliers // the clock of the worker and the audio context clock can drift apart over time From 1af2da0063c134bca9b99c9b9f39f49e0950a037 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 7 Mar 2024 12:00:27 +0100 Subject: [PATCH 27/69] remove changes to prevent conflicts with draw branch --- packages/core/draw.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 4b3706db..7a9454f2 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -124,13 +124,11 @@ export class Drawer { const lookahead = this.drawTime[1]; // calculate current frame time (think right side of screen for pianoroll) const phase = this.scheduler.now() + lookahead; - // first frame just captures the phase if (this.lastFrame === null) { this.lastFrame = phase; return; } - // query haps from last frame till now. take last 100ms max const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase); this.lastFrame = phase; @@ -155,7 +153,6 @@ export class Drawer { return; } // TODO: scheduler.now() seems to move even when it's stopped, this hints at a bug... - t = t ?? scheduler.now(); this.scheduler = scheduler; let [_, lookahead] = this.drawTime; From 619ffdd5e14f45bb9077426345752ea6882d785c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 7 Mar 2024 12:01:08 +0100 Subject: [PATCH 28/69] rename zyklus --- packages/core/cyclist.mjs | 2 +- packages/core/{zyklus.js => zyklus.mjs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/core/{zyklus.js => zyklus.mjs} (100%) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index a8ccd30f..819c31bc 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import createClock from './zyklus'; +import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; export class Cyclist { diff --git a/packages/core/zyklus.js b/packages/core/zyklus.mjs similarity index 100% rename from packages/core/zyklus.js rename to packages/core/zyklus.mjs From 2fcbffeaf900482eefee0d4b404a42b7ebb44c30 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 7 Mar 2024 12:14:17 +0100 Subject: [PATCH 29/69] add sync flag for neocyclist --- packages/core/repl.mjs | 8 ++++++-- website/src/repl/Repl.jsx | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 0ec106af..1d5a5e7a 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -1,4 +1,5 @@ import { NeoCyclist } from './neocyclist.mjs'; +import { Cyclist } from './cyclist.mjs'; import { evaluate as _evaluate } from './evaluate.mjs'; import { logger } from './logger.mjs'; import { setTime } from './time.mjs'; @@ -15,6 +16,7 @@ export function repl({ onToggle, editPattern, onUpdateState, + sync = false, }) { const state = { schedulerError: undefined, @@ -35,14 +37,16 @@ export function repl({ onUpdateState?.(state); }; - const scheduler = new NeoCyclist({ + const schedulerOptions = { onTrigger: getTrigger({ defaultOutput, getTime }), getTime, onToggle: (started) => { updateState({ started }); onToggle?.(started); }, - }); + }; + + const scheduler = sync ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions); let pPatterns = {}; let allTransform; diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 57bdb1d1..b5666467 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -56,6 +56,7 @@ export function Repl({ embedded = false }) { }); }; const editor = new StrudelMirror({ + sync: true, defaultOutput: webaudioOutput, getTime: () => getAudioContext().currentTime, transpiler, From 8dba865ca76fd5b5675dd13168dac7cf577045a9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 7 Mar 2024 12:35:39 +0100 Subject: [PATCH 30/69] add note to future --- packages/core/clockworker.js | 3 +++ packages/core/zyklus.mjs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index db9ef123..e16da478 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -1,5 +1,8 @@ // eslint-disable-next-line no-undef importScripts('./neozyklus.js'); +// TODO: swap below line with above one when firefox supports esm imports in service workers +// see https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker?retiredLocale=de#browser_compatibility +// import createClock from './zyklus.mjs'; function getTime() { const precision = 10 ** 4; diff --git a/packages/core/zyklus.mjs b/packages/core/zyklus.mjs index 3d25b054..d3c48667 100644 --- a/packages/core/zyklus.mjs +++ b/packages/core/zyklus.mjs @@ -23,7 +23,7 @@ function createClock( // callback as long as we're inside the lookahead while (phase < lookahead) { phase = Math.round(phase * precision) / precision; - phase >= t && callback(phase, duration, tick); + phase >= t && callback(phase, duration, tick, t); phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? phase += duration; // increment phase by duration tick++; From 85fee0cc01a0f6c8cdb500cf8032ccaeef1ecf36 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 8 Mar 2024 00:53:48 +0100 Subject: [PATCH 31/69] add claviature package --- packages/claviature/Claviature.jsx | 29 ++++ packages/claviature/index.mjs | 1 + packages/claviature/package.json | 42 ++++++ packages/claviature/vite.config.js | 20 +++ pnpm-lock.yaml | 219 +++++++++++++++++++++++++---- website/package.json | 1 + website/src/repl/util.mjs | 1 + 7 files changed, 286 insertions(+), 27 deletions(-) create mode 100644 packages/claviature/Claviature.jsx create mode 100644 packages/claviature/index.mjs create mode 100644 packages/claviature/package.json create mode 100644 packages/claviature/vite.config.js diff --git a/packages/claviature/Claviature.jsx b/packages/claviature/Claviature.jsx new file mode 100644 index 00000000..50dd2cfd --- /dev/null +++ b/packages/claviature/Claviature.jsx @@ -0,0 +1,29 @@ +import { createSignal, For } from 'solid-js'; +import { customElement } from 'solid-element'; +import { getClaviature } from 'claviature'; +import { Dynamic } from 'solid-js/web'; + +customElement('strudel-claviature', { someProp: 'one', otherProp: 'two' }, (props, { element }) => { + const svg = getClaviature({ + options: { + range: ['A1', 'C4'], + colorize: [{ keys: ['C3', 'E3', 'G3'], color: 'yellow' }], + }, + }); + const [activeNotes, setActiveNotes] = createSignal([]); + return ( +
+ + + {(el) => { + return ( + + {el.value} + + ); + }} + + +
+ ); +}); diff --git a/packages/claviature/index.mjs b/packages/claviature/index.mjs new file mode 100644 index 00000000..281c6ab8 --- /dev/null +++ b/packages/claviature/index.mjs @@ -0,0 +1 @@ +export * from './Claviature.jsx'; diff --git a/packages/claviature/package.json b/packages/claviature/package.json new file mode 100644 index 00000000..01ae8100 --- /dev/null +++ b/packages/claviature/package.json @@ -0,0 +1,42 @@ +{ + "name": "@strudel/claviature", + "version": "1.0.1", + "description": "Claviature component for Strudel", + "main": "dist/index.mjs", + "type": "module", + "publishConfig": { + "main": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "watch": "vite build --watch", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "titdalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*", + "claviature": "^0.1.0", + "solid-element": "^1.8.0", + "solid-js": "^1.8.15", + "vite-plugin-solid": "^2.10.1" + }, + "devDependencies": { + "vite": "^5.0.10" + } +} diff --git a/packages/claviature/vite.config.js b/packages/claviature/vite.config.js new file mode 100644 index 00000000..4ed5702c --- /dev/null +++ b/packages/claviature/vite.config.js @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite'; +import { dependencies } from './package.json'; +import { resolve } from 'path'; +import solid from 'vite-plugin-solid'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [solid()], + build: { + lib: { + entry: resolve(__dirname, 'index.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'index.mjs' })[ext], + }, + rollupOptions: { + external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b31cc8b..9cd9fb87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,7 +96,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 examples/headless-repl: dependencies: @@ -106,7 +106,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 examples/minimal-repl: dependencies: @@ -128,7 +128,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 examples/superdough: dependencies: @@ -138,7 +138,29 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 + + packages/claviature: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + claviature: + specifier: ^0.1.0 + version: 0.1.0 + solid-element: + specifier: ^1.8.0 + version: 1.8.0(solid-js@1.8.15) + solid-js: + specifier: ^1.8.15 + version: 1.8.15 + vite-plugin-solid: + specifier: ^2.10.1 + version: 2.10.1(solid-js@1.8.15)(vite@5.0.11) + devDependencies: + vite: + specifier: ^5.0.10 + version: 5.0.11(@types/node@20.10.6) packages/codemirror: dependencies: @@ -193,7 +215,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 packages/core: dependencies: @@ -203,7 +225,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 vitest: specifier: ^1.1.0 version: 1.1.0(@vitest/ui@1.1.0) @@ -222,7 +244,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 packages/desktopbridge: dependencies: @@ -249,7 +271,7 @@ importers: version: 5.8.1 vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 packages/midi: dependencies: @@ -265,7 +287,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 packages/mini: dependencies: @@ -278,7 +300,7 @@ importers: version: 3.0.2 vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 vitest: specifier: ^1.1.0 version: 1.1.0(@vitest/ui@1.1.0) @@ -297,7 +319,7 @@ importers: version: 5.8.1 vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 packages/repl: dependencies: @@ -337,7 +359,7 @@ importers: version: 5.12.0 vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 packages/serial: dependencies: @@ -347,7 +369,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 packages/soundfonts: dependencies: @@ -369,7 +391,7 @@ importers: version: 3.3.2 vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 packages/superdough: dependencies: @@ -379,7 +401,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 packages/tonal: dependencies: @@ -398,7 +420,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 vitest: specifier: ^1.1.0 version: 1.1.0(@vitest/ui@1.1.0) @@ -423,7 +445,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 vitest: specifier: ^1.1.0 version: 1.1.0(@vitest/ui@1.1.0) @@ -451,7 +473,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 packages/webaudio: dependencies: @@ -464,7 +486,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 packages/xen: dependencies: @@ -474,7 +496,7 @@ importers: devDependencies: vite: specifier: ^5.0.10 - version: 5.0.10(@types/node@20.10.6) + version: 5.0.10 vitest: specifier: ^1.1.0 version: 1.1.0(@vitest/ui@1.1.0) @@ -523,6 +545,9 @@ importers: '@nanostores/react': specifier: ^0.7.1 version: 0.7.1(nanostores@0.9.5)(react@18.2.0) + '@strudel/claviature': + specifier: workspace:* + version: link:../packages/claviature '@strudel/codemirror': specifier: workspace:* version: link:../packages/codemirror @@ -1081,6 +1106,13 @@ packages: '@babel/types': 7.23.6 dev: true + /@babel/helper-module-imports@7.18.6: + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + dev: false + /@babel/helper-module-imports@7.22.15: resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} engines: {node: '>=6.9.0'} @@ -2382,6 +2414,7 @@ packages: cpu: [arm64] os: [android] requiresBuild: true + dev: true optional: true /@esbuild/android-arm@0.19.11: @@ -2398,6 +2431,7 @@ packages: cpu: [arm] os: [android] requiresBuild: true + dev: true optional: true /@esbuild/android-x64@0.19.11: @@ -2414,6 +2448,7 @@ packages: cpu: [x64] os: [android] requiresBuild: true + dev: true optional: true /@esbuild/darwin-arm64@0.19.11: @@ -2430,6 +2465,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: true optional: true /@esbuild/darwin-x64@0.19.11: @@ -2446,6 +2482,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: true optional: true /@esbuild/freebsd-arm64@0.19.11: @@ -2462,6 +2499,7 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true + dev: true optional: true /@esbuild/freebsd-x64@0.19.11: @@ -2478,6 +2516,7 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true + dev: true optional: true /@esbuild/linux-arm64@0.19.11: @@ -2494,6 +2533,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-arm@0.19.11: @@ -2510,6 +2550,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-ia32@0.19.11: @@ -2526,6 +2567,7 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-loong64@0.19.11: @@ -2542,6 +2584,7 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-mips64el@0.19.11: @@ -2558,6 +2601,7 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-ppc64@0.19.11: @@ -2574,6 +2618,7 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-riscv64@0.19.11: @@ -2590,6 +2635,7 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-s390x@0.19.11: @@ -2606,6 +2652,7 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-x64@0.19.11: @@ -2622,6 +2669,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/netbsd-x64@0.19.11: @@ -2638,6 +2686,7 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true + dev: true optional: true /@esbuild/openbsd-x64@0.19.11: @@ -2654,6 +2703,7 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true + dev: true optional: true /@esbuild/sunos-x64@0.19.11: @@ -2670,6 +2720,7 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true + dev: true optional: true /@esbuild/win32-arm64@0.19.11: @@ -2686,6 +2737,7 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true + dev: true optional: true /@esbuild/win32-ia32@0.19.11: @@ -2702,6 +2754,7 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true + dev: true optional: true /@esbuild/win32-x64@0.19.11: @@ -2718,6 +2771,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: true optional: true /@eslint-community/eslint-utils@4.4.0(eslint@8.56.0): @@ -5346,8 +5400,8 @@ packages: tsconfck: 3.0.0(typescript@5.3.3) unist-util-visit: 5.0.0 vfile: 6.0.1 - vite: 5.0.10(@types/node@20.10.6) - vitefu: 0.2.5(vite@5.0.10) + vite: 5.0.11(@types/node@20.10.6) + vitefu: 0.2.5(vite@5.0.11) which-pm: 2.1.1 yargs-parser: 21.1.1 zod: 3.22.4 @@ -5424,6 +5478,19 @@ packages: resolution: {integrity: sha512-3AN/9V/rKuv90NG65m4tTHsI04XrCKsWbztIcW7a8H5iIN7WlvWucRtVV0V/rT4QvtA11n5Vmp20fLwfMWqp6g==} dev: false + /babel-plugin-jsx-dom-expressions@0.37.17(@babel/core@7.23.7): + resolution: {integrity: sha512-1bv8rOTzs6TR3DVyVZ7ElxyPEhnS556FMWRIsB3gBPfkn/cSKaLvXLGk+X1lvI+SzcUo4G+UcmJrn3vr1ig8mQ==} + peerDependencies: + '@babel/core': ^7.20.12 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.23.7) + '@babel/types': 7.23.6 + html-entities: 2.3.3 + validate-html-nesting: 1.2.2 + dev: false + /babel-plugin-polyfill-corejs2@0.4.7(@babel/core@7.23.7): resolution: {integrity: sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==} peerDependencies: @@ -5460,6 +5527,15 @@ packages: - supports-color dev: true + /babel-preset-solid@1.8.15(@babel/core@7.23.7): + resolution: {integrity: sha512-P2yOQbB7Hn/m4YvpXV6ExHIMcgNWXWXcvY4kJzG3yqAB3hKS58OZRsvJ7RObsZWqXRvZTITBIwnpK0BMGu+ZIQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.7 + babel-plugin-jsx-dom-expressions: 0.37.17(@babel/core@7.23.7) + dev: false + /bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -6069,6 +6145,10 @@ packages: dot-prop: 5.3.0 dev: true + /component-register@0.8.3: + resolution: {integrity: sha512-/0u8ov0WPWi2FL78rgB9aFOcfY8pJT4jP/l9NTOukGNLVQ6hk35sEJE1RkEnNQU3yk48Qr7HlDQjRQKEVfgeWg==} + dev: false + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -6828,6 +6908,7 @@ packages: '@esbuild/win32-arm64': 0.19.5 '@esbuild/win32-ia32': 0.19.5 '@esbuild/win32-x64': 0.19.5 + dev: true /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -8085,6 +8166,10 @@ packages: lru-cache: 10.1.0 dev: true + /html-entities@2.3.3: + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + dev: false + /html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -8607,6 +8692,11 @@ packages: call-bind: 1.0.5 dev: true + /is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + dev: false + /is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -9573,6 +9663,13 @@ packages: yargs-parser: 20.2.9 dev: true + /merge-anything@5.1.7: + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} + engines: {node: '>=12.13'} + dependencies: + is-what: 4.1.16 + dev: false + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -12138,6 +12235,20 @@ packages: randombytes: 2.1.0 dev: true + /seroval-plugins@1.0.4(seroval@1.0.4): + resolution: {integrity: sha512-DQ2IK6oQVvy8k+c2V5x5YCtUa/GGGsUwUBNN9UqohrZ0rWdUapBFpNMYP1bCyRHoxOJjdKGl+dieacFIpU/i1A==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + dependencies: + seroval: 1.0.4 + dev: false + + /seroval@1.0.4: + resolution: {integrity: sha512-qQs/N+KfJu83rmszFQaTxcoJoPn6KNUruX4KmnmyD0oZkUoiNvJ1rpdYKDf4YHM05k+HOgCxa3yvf15QbVijGg==} + engines: {node: '>=10'} + dev: false + /server-destroy@1.0.1: resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==} @@ -12340,6 +12451,34 @@ packages: smart-buffer: 4.2.0 dev: true + /solid-element@1.8.0(solid-js@1.8.15): + resolution: {integrity: sha512-DG8HBCej5kNExUiFbVG8OFZojMGcLF8keXdGLEcHXBYtJ7zhm+a8HJnl5lfmBlTYGRk4ApgoBvlwH1ibg7quaQ==} + peerDependencies: + solid-js: ^1.8.0 + dependencies: + component-register: 0.8.3 + solid-js: 1.8.15 + dev: false + + /solid-js@1.8.15: + resolution: {integrity: sha512-d0QP/efr3UVcwGgWVPveQQ0IHOH6iU7yUhc2piy8arNG8wxKmvUy1kFxyF8owpmfCWGB87usDKMaVnsNYZm+Vw==} + dependencies: + csstype: 3.1.1 + seroval: 1.0.4 + seroval-plugins: 1.0.4(seroval@1.0.4) + dev: false + + /solid-refresh@0.6.3(solid-js@1.8.15): + resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} + peerDependencies: + solid-js: ^1.3 + dependencies: + '@babel/generator': 7.23.6 + '@babel/helper-module-imports': 7.22.15 + '@babel/types': 7.23.6 + solid-js: 1.8.15 + dev: false + /sort-array@4.1.5: resolution: {integrity: sha512-Ya4peoS1fgFN42RN1REk2FgdNOeLIEMKFGJvs7VTP3OklF8+kl2SkpVliZ4tk/PurWsrWRsdNdU+tgyOBkB9sA==} engines: {node: '>=10'} @@ -13455,6 +13594,10 @@ packages: hasBin: true dev: true + /validate-html-nesting@1.2.2: + resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} + dev: false + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -13565,7 +13708,29 @@ packages: - supports-color dev: true - /vite@5.0.10(@types/node@20.10.6): + /vite-plugin-solid@2.10.1(solid-js@1.8.15)(vite@5.0.11): + resolution: {integrity: sha512-kfVdNLWaJqaJVL52U6iCCKNW/nXE7bS1VVGOWPGllOkJfcNILymVSY0LCBLSnyy0iYnRtrXpiHm14rMuzeC7CA==} + peerDependencies: + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* + solid-js: ^1.7.2 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + '@testing-library/jest-dom': + optional: true + dependencies: + '@babel/core': 7.23.7 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.8.15(@babel/core@7.23.7) + merge-anything: 5.1.7 + solid-js: 1.8.15 + solid-refresh: 0.6.3(solid-js@1.8.15) + vite: 5.0.11(@types/node@20.10.6) + vitefu: 0.2.5(vite@5.0.11) + transitivePeerDependencies: + - supports-color + dev: false + + /vite@5.0.10: resolution: {integrity: sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -13593,12 +13758,12 @@ packages: terser: optional: true dependencies: - '@types/node': 20.10.6 esbuild: 0.19.5 postcss: 8.4.32 rollup: 4.9.2 optionalDependencies: fsevents: 2.3.3 + dev: true /vite@5.0.11(@types/node@20.10.6): resolution: {integrity: sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==} @@ -13629,13 +13794,13 @@ packages: optional: true dependencies: '@types/node': 20.10.6 - esbuild: 0.19.5 + esbuild: 0.19.11 postcss: 8.4.32 rollup: 4.9.2 optionalDependencies: fsevents: 2.3.3 - /vitefu@0.2.5(vite@5.0.10): + /vitefu@0.2.5(vite@5.0.11): resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} peerDependencies: vite: ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -13643,7 +13808,7 @@ packages: vite: optional: true dependencies: - vite: 5.0.10(@types/node@20.10.6) + vite: 5.0.11(@types/node@20.10.6) /vitest@1.1.0(@vitest/ui@1.1.0): resolution: {integrity: sha512-oDFiCrw7dd3Jf06HoMtSRARivvyjHJaTxikFxuqJjO76U436PqlVw1uLn7a8OSPrhSfMGVaRakKpA2lePdw79A==} diff --git a/website/package.json b/website/package.json index af100889..13bef75d 100644 --- a/website/package.json +++ b/website/package.json @@ -24,6 +24,7 @@ "@heroicons/react": "^2.1.1", "@nanostores/persistent": "^0.9.1", "@nanostores/react": "^0.7.1", + "@strudel/claviature": "workspace:*", "@strudel/codemirror": "workspace:*", "@strudel/core": "workspace:*", "@strudel/csound": "workspace:*", diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 397e2801..2d809ad6 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -81,6 +81,7 @@ export function loadModules() { import('@strudel/serial'), import('@strudel/soundfonts'), import('@strudel/csound'), + import('@strudel/claviature'), ]; if (isTauri()) { modules = modules.concat([ From 9b3509c06278152db59aaa00f04643fba6e9d1d2 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 8 Mar 2024 00:55:19 +0100 Subject: [PATCH 32/69] global watch task --- package.json | 1 + packages/core/package.json | 1 + packages/mini/package.json | 1 + website/package.json | 1 + 4 files changed, 4 insertions(+) diff --git a/package.json b/package.json index 741cf070..ea1373aa 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "repl": "npm run prestart && cd website && npm run dev", "start": "npm run prestart && cd website && npm run dev", "dev": "npm run prestart && cd website && npm run dev", + "watch": "pnpm prestart && pnpm --parallel --filter {./website/**}... --filter \"@strudel/website\" watch", "build": "npm run prebuild && cd website && npm run build", "preview": "cd website && npm run preview", "osc": "cd packages/osc && npm run server", diff --git a/packages/core/package.json b/packages/core/package.json index 7f59ed41..930acee8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,6 +10,7 @@ "scripts": { "test": "vitest run", "build": "vite build", + "watch": "vite build --watch", "prepublishOnly": "pnpm build" }, "repository": { diff --git a/packages/mini/package.json b/packages/mini/package.json index cb4f459c..28848404 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -11,6 +11,7 @@ "test": "vitest run", "build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs", "build": "vite build", + "watch": "vite build --watch", "prepublishOnly": "npm run build" }, "repository": { diff --git a/website/package.json b/website/package.json index 13bef75d..c954e52f 100644 --- a/website/package.json +++ b/website/package.json @@ -5,6 +5,7 @@ "private": true, "scripts": { "dev": "astro dev --host 0.0.0.0", + "watch": "astro dev --host 0.0.0.0", "start": "astro dev", "check": "astro check && tsc", "build": "astro build", From 43c40f7bb011fd92cec306d50ddcabd5e6cea9d1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 8 Mar 2024 01:07:17 +0100 Subject: [PATCH 33/69] no need for publishConfig --- packages/claviature/package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/claviature/package.json b/packages/claviature/package.json index 01ae8100..194c86b0 100644 --- a/packages/claviature/package.json +++ b/packages/claviature/package.json @@ -4,9 +4,6 @@ "description": "Claviature component for Strudel", "main": "dist/index.mjs", "type": "module", - "publishConfig": { - "main": "dist/index.mjs" - }, "scripts": { "build": "vite build", "watch": "vite build --watch", From 30a5176090628752ca84f934b2c11933e7bb39bc Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 7 Mar 2024 23:04:46 -0500 Subject: [PATCH 34/69] fix android support --- packages/core/repl.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 1d5a5e7a..aa8762d8 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -46,7 +46,9 @@ export function repl({ }, }; - const scheduler = sync ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions); + // NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome + const scheduler = + sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions); let pPatterns = {}; let allTransform; From 112ca1875a8f2d65fa5168bc70f5c21e29a64bee Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 10 Mar 2024 14:35:43 -0400 Subject: [PATCH 35/69] stop clock if all stopped --- packages/core/clockworker.js | 23 +++++++++++++++-------- packages/core/neocyclist.mjs | 3 ++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index e16da478..c5442422 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -10,10 +10,11 @@ function getTime() { return Math.round(seconds * precision) / precision; } -let numPorts = 0; let num_cycles_at_cps_change = 0; let num_ticks_since_cps_change = 0; let cps = 0.5; +// {id: {started: boolean}} +const clients = new Map(); const duration = 0.1; const channel = new BroadcastChannel('strudeltick'); @@ -37,18 +38,23 @@ const sendTick = (phase, duration, tick, time) => { const clock = this.createClock(getTime, sendTick, duration); let started = false; -const startClock = () => { +const startClock = (id) => { + clients.set(id, { started: true }); if (started) { return; } clock.start(); started = true; }; -const stopClock = async () => { - //dont stop the clock if mutliple instances are using it... - if (!started || numPorts !== 1) { +const stopClock = async (id) => { + clients.set(id, { started: false }); + + const otherClientStarted = Array.from(clients.values()).some((c) => c.started); + //dont stop the clock if other instances are running... + if (!started || otherClientStarted) { return; } + clock.stop(); setCycle(0); started = false; @@ -77,9 +83,9 @@ const processMessage = (message) => { } case 'toggle': { if (payload.started) { - startClock(); + startClock(message.id); } else { - stopClock(); + stopClock(message.id); } break; } @@ -89,8 +95,9 @@ const processMessage = (message) => { this.onconnect = function (e) { // the incoming port const port = e.ports[0]; - numPorts = numPorts + 1; + port.addEventListener('message', function (e) { + console.log(e.data); processMessage(e.data); }); port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 3cfb8d64..1ffb6fda 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -17,6 +17,7 @@ export class NeoCyclist { this.onToggle = onToggle; this.latency = 0.1; // fixed trigger time offset this.cycle = 0; + this.id = Math.round(Date.now() * Math.random()); this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url)); this.worker.port.start(); @@ -107,7 +108,7 @@ export class NeoCyclist { }; } sendMessage(type, payload) { - this.worker.port.postMessage({ type, payload }); + this.worker.port.postMessage({ type, payload, id: this.id }); } now() { From 0a1da6f6514df06f21d81d3e81fd65d9cf414019 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 14 Mar 2024 14:20:56 +0100 Subject: [PATCH 36/69] claviature lockfile --- pnpm-lock.yaml | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdbe177a..97ff4f66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,6 +140,28 @@ importers: specifier: ^5.0.10 version: 5.0.10 + packages/claviature: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + claviature: + specifier: ^0.1.0 + version: 0.1.0 + solid-element: + specifier: ^1.8.0 + version: 1.8.0(solid-js@1.8.15) + solid-js: + specifier: ^1.8.15 + version: 1.8.15 + vite-plugin-solid: + specifier: ^2.10.1 + version: 2.10.1(solid-js@1.8.15)(vite@5.0.11) + devDependencies: + vite: + specifier: ^5.0.10 + version: 5.0.11(@types/node@20.10.6) + packages/codemirror: dependencies: '@codemirror/autocomplete': @@ -13711,6 +13733,28 @@ packages: - supports-color dev: true + /vite-plugin-solid@2.10.1(solid-js@1.8.15)(vite@5.0.11): + resolution: {integrity: sha512-kfVdNLWaJqaJVL52U6iCCKNW/nXE7bS1VVGOWPGllOkJfcNILymVSY0LCBLSnyy0iYnRtrXpiHm14rMuzeC7CA==} + peerDependencies: + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* + solid-js: ^1.7.2 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + '@testing-library/jest-dom': + optional: true + dependencies: + '@babel/core': 7.23.7 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.8.15(@babel/core@7.23.7) + merge-anything: 5.1.7 + solid-js: 1.8.15 + solid-refresh: 0.6.3(solid-js@1.8.15) + vite: 5.0.11(@types/node@20.10.6) + vitefu: 0.2.5(vite@5.0.11) + transitivePeerDependencies: + - supports-color + dev: false + /vite@5.0.10: resolution: {integrity: sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==} engines: {node: ^18.0.0 || >=20.0.0} From f2e16f946cca3f99bb30ec2d72c62362bb6a2b7f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 14 Mar 2024 15:17:19 +0100 Subject: [PATCH 37/69] add working claviature method --- packages/claviature/Claviature.jsx | 25 ++++++++++++++----------- packages/claviature/index.mjs | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/packages/claviature/Claviature.jsx b/packages/claviature/Claviature.jsx index 50dd2cfd..ec79edf6 100644 --- a/packages/claviature/Claviature.jsx +++ b/packages/claviature/Claviature.jsx @@ -1,20 +1,23 @@ -import { createSignal, For } from 'solid-js'; +import { For } from 'solid-js'; import { customElement } from 'solid-element'; import { getClaviature } from 'claviature'; import { Dynamic } from 'solid-js/web'; -customElement('strudel-claviature', { someProp: 'one', otherProp: 'two' }, (props, { element }) => { - const svg = getClaviature({ - options: { - range: ['A1', 'C4'], - colorize: [{ keys: ['C3', 'E3', 'G3'], color: 'yellow' }], - }, - }); - const [activeNotes, setActiveNotes] = createSignal([]); +let defaultOptions = { + range: ['A1', 'C6'], +}; + +customElement('strudel-claviature', { options: JSON.stringify(defaultOptions) }, (props, { element }) => { + let svg = () => { + let c = getClaviature({ + options: JSON.parse(props.options), + }); + return c; + }; return (
- - + + {(el) => { return ( diff --git a/packages/claviature/index.mjs b/packages/claviature/index.mjs index 281c6ab8..c4ee4860 100644 --- a/packages/claviature/index.mjs +++ b/packages/claviature/index.mjs @@ -1 +1,24 @@ export * from './Claviature.jsx'; +import { Pattern } from '@strudel/core'; + +Pattern.prototype.claviature = function (options = {}) { + if (!window.claviature) { + window.claviature = document.createElement('strudel-claviature'); + window.claviature.style.position = 'absolute'; + window.claviature.style.bottom = 0; + window.claviature.style.left = 0; + document.body.append(window.claviature); + } + return this.onFrame((haps) => { + const keys = haps.map((h) => h.value.note); + // console.log('keys',keys); + window.claviature.setAttribute( + 'options', + JSON.stringify({ + ...options, + range: options.range || ['A2', 'C6'], + colorize: [{ keys: keys, color: options.color || 'steelblue' }], + }), + ); + }); +}; From cfdd4ac36ca45fa3fa520f369787e49c6cdf1c97 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 14 Mar 2024 15:28:06 +0100 Subject: [PATCH 38/69] add readme --- packages/claviature/README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 packages/claviature/README.md diff --git a/packages/claviature/README.md b/packages/claviature/README.md new file mode 100644 index 00000000..eb540b2e --- /dev/null +++ b/packages/claviature/README.md @@ -0,0 +1,29 @@ +# @strudel/claviature + +adds a `Patter.claviature` method that renders a [claviature](https://www.npmjs.com/package/claviature). + +example usage: + +```js +chord("").voicing().piano() + .claviature() +``` + +All [claviature options](https://www.npmjs.com/package/claviature#options) will work. + +Here is an example that uses all available options: + +```js +chord("").voicing().piano() +.claviature({ + range: ['C1', 'C6'], // rendered note range + color: 'yellow', // highlighting color + palette: ['cyan', 'magenta'], + stroke: 'black', + scaleX: 1, scaleY: 1, + upperHeight: 80, + lowerHeight: 50 +}) +``` + +Note: The `Pattern.claviature` method uses the `colorization` option internally, so don't override that and use the `color` option for changing the highlighting color. From 1e75f045b7bf1510574bf065d6cec8ecd096f4bd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 14 Mar 2024 15:31:50 +0100 Subject: [PATCH 39/69] remove other watch tasks for now --- packages/core/package.json | 1 - packages/mini/package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 930acee8..7f59ed41 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,7 +10,6 @@ "scripts": { "test": "vitest run", "build": "vite build", - "watch": "vite build --watch", "prepublishOnly": "pnpm build" }, "repository": { diff --git a/packages/mini/package.json b/packages/mini/package.json index 28848404..cb4f459c 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -11,7 +11,6 @@ "test": "vitest run", "build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs", "build": "vite build", - "watch": "vite build --watch", "prepublishOnly": "npm run build" }, "repository": { From 29dab578e7488f254cbe20105f9e406c1b58f6b1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 14 Mar 2024 23:49:38 +0100 Subject: [PATCH 40/69] can now load claviature as a codemirror widget --- packages/codemirror/codemirror.mjs | 9 ++- packages/codemirror/index.mjs | 1 + packages/codemirror/slider.mjs | 38 ++++++------ packages/codemirror/widget.mjs | 95 ++++++++++++++++++++++++++++++ packages/transpiler/transpiler.mjs | 32 +++++++++- 5 files changed, 152 insertions(+), 23 deletions(-) create mode 100644 packages/codemirror/widget.mjs diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 512c873a..c06ac4a0 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -20,7 +20,8 @@ import { flash, isFlashEnabled } from './flash.mjs'; import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs'; import { keybindings } from './keybindings.mjs'; import { initTheme, activateTheme, theme } from './themes.mjs'; -import { updateWidgets, sliderPlugin } from './slider.mjs'; +import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; +import { widgetPlugin, updateWidgets } from './widget.mjs'; import { persistentAtom } from '@nanostores/persistent'; const extensions = { @@ -72,6 +73,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo ...initialSettings, javascript(), sliderPlugin, + widgetPlugin, // indentOnInput(), // works without. already brought with javascript extension? // bracketMatching(), // does not do anything closeBrackets(), @@ -187,7 +189,10 @@ export class StrudelMirror { // remember for when highlighting is toggled on this.miniLocations = options.meta?.miniLocations; this.widgets = options.meta?.widgets; - updateWidgets(this.editor, this.widgets); + const sliders = this.widgets.filter((w) => w.type === 'slider'); + updateSliderWidgets(this.editor, sliders); + const widgets = this.widgets.filter((w) => w.type !== 'slider'); + updateWidgets(this.editor, widgets); updateMiniLocations(this.editor, this.miniLocations); replOptions?.afterEval?.(options); this.adjustDrawTime(); diff --git a/packages/codemirror/index.mjs b/packages/codemirror/index.mjs index 8f2d1630..3a5f2a23 100644 --- a/packages/codemirror/index.mjs +++ b/packages/codemirror/index.mjs @@ -3,3 +3,4 @@ export * from './highlight.mjs'; export * from './flash.mjs'; export * from './slider.mjs'; export * from './themes.mjs'; +export * from './widget.mjs'; diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index a46f0e1e..21f744bb 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -1,10 +1,27 @@ import { ref, pure } from '@strudel/core'; import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view'; -import { StateEffect, StateField } from '@codemirror/state'; +import { StateEffect } from '@codemirror/state'; + +export const setSliderWidgets = StateEffect.define(); + +export const updateSliderWidgets = (view, widgets) => { + view.dispatch({ effects: setSliderWidgets.of(widgets) }); +}; export let sliderValues = {}; const getSliderID = (from) => `slider_${from}`; +function getSliders(widgetConfigs, view) { + return widgetConfigs + .filter((w) => w.type === 'slider') + .map(({ from, to, value, min, max, step }) => { + return Decoration.widget({ + widget: new SliderWidget(value, min, max, from, to, step, view), + side: 0, + }).range(from /* , to */); + }); +} + export class SliderWidget extends WidgetType { constructor(value, min, max, from, to, step, view) { super(); @@ -60,21 +77,6 @@ export class SliderWidget extends WidgetType { } } -export const setWidgets = StateEffect.define(); - -export const updateWidgets = (view, widgets) => { - view.dispatch({ effects: setWidgets.of(widgets) }); -}; - -function getWidgets(widgetConfigs, view) { - return widgetConfigs.map(({ from, to, value, min, max, step }) => { - return Decoration.widget({ - widget: new SliderWidget(value, min, max, from, to, step, view), - side: 0, - }).range(from /* , to */); - }); -} - export const sliderPlugin = ViewPlugin.fromClass( class { decorations; //: DecorationSet @@ -99,8 +101,8 @@ export const sliderPlugin = ViewPlugin.fromClass( } } for (let e of tr.effects) { - if (e.is(setWidgets)) { - this.decorations = Decoration.set(getWidgets(e.value, update.view)); + if (e.is(setSliderWidgets)) { + this.decorations = Decoration.set(getSliders(e.value, update.view)); } } }); diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs new file mode 100644 index 00000000..3df38d8b --- /dev/null +++ b/packages/codemirror/widget.mjs @@ -0,0 +1,95 @@ +import { StateEffect, StateField } from '@codemirror/state'; +import { Decoration, EditorView, WidgetType } from '@codemirror/view'; +import { Pattern } from '@strudel/core'; + +const getWidgetID = (from) => `ui_${from}`; + +Pattern.prototype.ui = function (id, value) { + // TODO: make this work with any web component + return this.onFrame((haps) => { + let el = document.getElementById(id); + if (el) { + let options = {}; + const keys = haps.map((h) => h.value.note); + el.setAttribute( + 'options', + JSON.stringify({ + ...options, + range: options.range || ['A2', 'C6'], + colorize: [{ keys: keys, color: options.color || 'steelblue' }], + }), + ); + } + }); +}; + +export const addWidget = StateEffect.define({ + map: ({ from, to }, change) => { + return { from: change.mapPos(from), to: change.mapPos(to) }; + }, +}); + +export const updateWidgets = (view, widgets) => { + view.dispatch({ effects: addWidget.of(widgets) }); +}; + +function getWidgets(widgetConfigs, view) { + return widgetConfigs.map(({ from, to }) => { + return Decoration.widget({ + widget: new BlockWidget(view, from), + side: 0, + block: true, + }).range(to); + }); +} + +const widgetField = StateField.define( + /* */ { + create() { + return Decoration.none; + }, + update(widgets, tr) { + widgets = widgets.map(tr.changes); + for (let e of tr.effects) { + if (e.is(addWidget)) { + try { + widgets = widgets.update({ + filter: () => false, + add: getWidgets(e.value), + }); + } catch (error) { + console.log('err', error); + } + } + } + return widgets; + }, + provide: (f) => EditorView.decorations.from(f), + }, +); + +export class BlockWidget extends WidgetType { + constructor(view, from) { + super(); + this.view = view; + this.from = from; + } + eq() { + return true; + } + toDOM() { + const id = getWidgetID(this.from); // matches id generated in transpiler + let el = document.getElementById(id); + if (!el) { + // TODO: make this work with any web component + el = document.createElement('strudel-claviature'); + el.id = id; + } + return el; + } + ignoreEvent(e) { + return true; + } +} + +export const widgetPlugin = [widgetField]; diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 72f2e851..48db38a6 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -34,7 +34,7 @@ export function transpiler(input, options = {}) { emitMiniLocations && collectMiniLocations(value, node); return this.replace(miniWithLocation(value, node)); } - if (isWidgetFunction(node)) { + if (isSliderFunction(node)) { emitWidgets && widgets.push({ from: node.arguments[0].start, @@ -43,6 +43,16 @@ export function transpiler(input, options = {}) { min: node.arguments[1]?.value ?? 0, max: node.arguments[2]?.value ?? 1, step: node.arguments[3]?.value, + type: 'slider', + }); + return this.replace(sliderWithLocation(node)); + } + if (isUIFunction(node)) { + emitWidgets && + widgets.push({ + from: node.arguments[0].start, + to: node.end, + type: node.arguments[0].value, }); return this.replace(widgetWithLocation(node)); } @@ -105,11 +115,15 @@ function miniWithLocation(value, node) { // these functions are connected to @strudel/codemirror -> slider.mjs // maybe someday there will be pluggable transpiler functions, then move this there -function isWidgetFunction(node) { +function isSliderFunction(node) { return node.type === 'CallExpression' && node.callee.name === 'slider'; } -function widgetWithLocation(node) { +function isUIFunction(node) { + return node.type === 'CallExpression' && node.callee.property?.name === 'ui'; +} + +function sliderWithLocation(node) { const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id // add loc as identifier to first argument // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) @@ -122,6 +136,18 @@ function widgetWithLocation(node) { return node; } +function widgetWithLocation(node) { + const id = 'ui_' + node.arguments[0].start; // use loc of first arg for id + // add loc as identifier to first argument + // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) + node.arguments.unshift({ + type: 'Literal', + value: id, + raw: id, + }); + return node; +} + function isBareSamplesCall(node, parent) { return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression'; } From 82c4926f197c5a189c8562c24abaa6cb4bbcc6c7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Mar 2024 00:27:16 +0100 Subject: [PATCH 41/69] allow any web component to become a widget --- packages/claviature/index.mjs | 16 +++++------- packages/claviature/package.json | 1 + packages/codemirror/widget.mjs | 39 +++++++----------------------- packages/transpiler/transpiler.mjs | 23 +++++++++++++----- pnpm-lock.yaml | 3 +++ 5 files changed, 36 insertions(+), 46 deletions(-) diff --git a/packages/claviature/index.mjs b/packages/claviature/index.mjs index c4ee4860..c39237d5 100644 --- a/packages/claviature/index.mjs +++ b/packages/claviature/index.mjs @@ -1,18 +1,14 @@ export * from './Claviature.jsx'; import { Pattern } from '@strudel/core'; +import { registerWidget } from '@strudel/transpiler'; -Pattern.prototype.claviature = function (options = {}) { - if (!window.claviature) { - window.claviature = document.createElement('strudel-claviature'); - window.claviature.style.position = 'absolute'; - window.claviature.style.bottom = 0; - window.claviature.style.left = 0; - document.body.append(window.claviature); - } +registerWidget('claviature', 'strudel-claviature'); + +Pattern.prototype.claviature = function (id, options = {}) { return this.onFrame((haps) => { const keys = haps.map((h) => h.value.note); - // console.log('keys',keys); - window.claviature.setAttribute( + let el = document.getElementById(id); + el?.setAttribute( 'options', JSON.stringify({ ...options, diff --git a/packages/claviature/package.json b/packages/claviature/package.json index 194c86b0..16e8f549 100644 --- a/packages/claviature/package.json +++ b/packages/claviature/package.json @@ -28,6 +28,7 @@ "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", + "@strudel/transpiler": "workspace:*", "claviature": "^0.1.0", "solid-element": "^1.8.0", "solid-js": "^1.8.15", diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 3df38d8b..3171d0df 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -1,27 +1,7 @@ import { StateEffect, StateField } from '@codemirror/state'; import { Decoration, EditorView, WidgetType } from '@codemirror/view'; -import { Pattern } from '@strudel/core'; -const getWidgetID = (from) => `ui_${from}`; - -Pattern.prototype.ui = function (id, value) { - // TODO: make this work with any web component - return this.onFrame((haps) => { - let el = document.getElementById(id); - if (el) { - let options = {}; - const keys = haps.map((h) => h.value.note); - el.setAttribute( - 'options', - JSON.stringify({ - ...options, - range: options.range || ['A2', 'C6'], - colorize: [{ keys: keys, color: options.color || 'steelblue' }], - }), - ); - } - }); -}; +const getWidgetID = (from) => `widget_${from}`; export const addWidget = StateEffect.define({ map: ({ from, to }, change) => { @@ -33,10 +13,10 @@ export const updateWidgets = (view, widgets) => { view.dispatch({ effects: addWidget.of(widgets) }); }; -function getWidgets(widgetConfigs, view) { - return widgetConfigs.map(({ from, to }) => { +function getWidgets(widgetConfigs) { + return widgetConfigs.map(({ to, type }) => { return Decoration.widget({ - widget: new BlockWidget(view, from), + widget: new BlockWidget(to, type), side: 0, block: true, }).range(to); @@ -69,20 +49,19 @@ const widgetField = StateField.define( ); export class BlockWidget extends WidgetType { - constructor(view, from) { + constructor(col, type) { super(); - this.view = view; - this.from = from; + this.col = col; + this.type = type; } eq() { return true; } toDOM() { - const id = getWidgetID(this.from); // matches id generated in transpiler + const id = getWidgetID(this.col); // matches id generated in transpiler let el = document.getElementById(id); if (!el) { - // TODO: make this work with any web component - el = document.createElement('strudel-claviature'); + el = document.createElement(this.type); el.id = id; } return el; diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 48db38a6..db805e14 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -3,6 +3,18 @@ import { parse } from 'acorn'; import escodegen from 'escodegen'; import { walk } from 'estree-walker'; +let widgetComponents = {}; +// this function allows registering a pattern method name and component name +// e.g. register('claviature', 'strudel-claviature') +// .. will map the Pattern method 'claviature' to the web component 'strudel-claviature' +// the transpiler will turn .claviature(...args) into .claviature(id, ...args) +// the widgetPlugin of @strudel/codemirror will automatically create an instance of 'strudel-claviature' +// .. so you only have to implement the actual .claviature method (or what you've called it..) + +export function registerWidget(name, tagName) { + widgetComponents[name] = tagName; +} + export function transpiler(input, options = {}) { const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; @@ -47,12 +59,11 @@ export function transpiler(input, options = {}) { }); return this.replace(sliderWithLocation(node)); } - if (isUIFunction(node)) { + if (isWidgetMethod(node)) { emitWidgets && widgets.push({ - from: node.arguments[0].start, to: node.end, - type: node.arguments[0].value, + type: widgetComponents[node.callee.property.name], }); return this.replace(widgetWithLocation(node)); } @@ -119,8 +130,8 @@ function isSliderFunction(node) { return node.type === 'CallExpression' && node.callee.name === 'slider'; } -function isUIFunction(node) { - return node.type === 'CallExpression' && node.callee.property?.name === 'ui'; +function isWidgetMethod(node) { + return node.type === 'CallExpression' && Object.keys(widgetComponents).includes(node.callee.property?.name); } function sliderWithLocation(node) { @@ -137,7 +148,7 @@ function sliderWithLocation(node) { } function widgetWithLocation(node) { - const id = 'ui_' + node.arguments[0].start; // use loc of first arg for id + const id = 'widget_' + node.end; // add loc as identifier to first argument // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) node.arguments.unshift({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97ff4f66..75c4e0a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,6 +145,9 @@ importers: '@strudel/core': specifier: workspace:* version: link:../core + '@strudel/transpiler': + specifier: workspace:* + version: link:../transpiler claviature: specifier: ^0.1.0 version: 0.1.0 From 708675077f2dcc9e3f942e02728e0347b1b77117 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Mar 2024 00:34:13 +0100 Subject: [PATCH 42/69] rename claviature package to widgets --- packages/claviature/index.mjs | 20 ------- .../{claviature => widgets}/Claviature.jsx | 19 ++++++ packages/{claviature => widgets}/README.md | 8 ++- packages/widgets/index.mjs | 1 + packages/{claviature => widgets}/package.json | 4 +- .../{claviature => widgets}/vite.config.js | 0 pnpm-lock.yaml | 60 +++++++++---------- website/package.json | 2 +- website/src/repl/util.mjs | 2 +- 9 files changed, 59 insertions(+), 57 deletions(-) delete mode 100644 packages/claviature/index.mjs rename packages/{claviature => widgets}/Claviature.jsx (57%) rename packages/{claviature => widgets}/README.md (81%) create mode 100644 packages/widgets/index.mjs rename packages/{claviature => widgets}/package.json (91%) rename packages/{claviature => widgets}/vite.config.js (100%) diff --git a/packages/claviature/index.mjs b/packages/claviature/index.mjs deleted file mode 100644 index c39237d5..00000000 --- a/packages/claviature/index.mjs +++ /dev/null @@ -1,20 +0,0 @@ -export * from './Claviature.jsx'; -import { Pattern } from '@strudel/core'; -import { registerWidget } from '@strudel/transpiler'; - -registerWidget('claviature', 'strudel-claviature'); - -Pattern.prototype.claviature = function (id, options = {}) { - return this.onFrame((haps) => { - const keys = haps.map((h) => h.value.note); - let el = document.getElementById(id); - el?.setAttribute( - 'options', - JSON.stringify({ - ...options, - range: options.range || ['A2', 'C6'], - colorize: [{ keys: keys, color: options.color || 'steelblue' }], - }), - ); - }); -}; diff --git a/packages/claviature/Claviature.jsx b/packages/widgets/Claviature.jsx similarity index 57% rename from packages/claviature/Claviature.jsx rename to packages/widgets/Claviature.jsx index ec79edf6..0d78d242 100644 --- a/packages/claviature/Claviature.jsx +++ b/packages/widgets/Claviature.jsx @@ -2,6 +2,8 @@ import { For } from 'solid-js'; import { customElement } from 'solid-element'; import { getClaviature } from 'claviature'; import { Dynamic } from 'solid-js/web'; +import { Pattern } from '@strudel/core'; +import { registerWidget } from '@strudel/transpiler'; let defaultOptions = { range: ['A1', 'C6'], @@ -30,3 +32,20 @@ customElement('strudel-claviature', { options: JSON.stringify(defaultOptions) },
); }); + +registerWidget('claviature', 'strudel-claviature'); + +Pattern.prototype.claviature = function (id, options = {}) { + return this.onFrame((haps) => { + const keys = haps.map((h) => h.value.note); + let el = document.getElementById(id); + el?.setAttribute( + 'options', + JSON.stringify({ + ...options, + range: options.range || ['A2', 'C6'], + colorize: [{ keys: keys, color: options.color || 'steelblue' }], + }), + ); + }); +}; diff --git a/packages/claviature/README.md b/packages/widgets/README.md similarity index 81% rename from packages/claviature/README.md rename to packages/widgets/README.md index eb540b2e..a5c36193 100644 --- a/packages/claviature/README.md +++ b/packages/widgets/README.md @@ -1,6 +1,10 @@ -# @strudel/claviature +# @strudel/widgets -adds a `Patter.claviature` method that renders a [claviature](https://www.npmjs.com/package/claviature). +adds UI widgets to codemirror + +## claviature + +`Patter.claviature` renders a [claviature](https://www.npmjs.com/package/claviature). example usage: diff --git a/packages/widgets/index.mjs b/packages/widgets/index.mjs new file mode 100644 index 00000000..281c6ab8 --- /dev/null +++ b/packages/widgets/index.mjs @@ -0,0 +1 @@ +export * from './Claviature.jsx'; diff --git a/packages/claviature/package.json b/packages/widgets/package.json similarity index 91% rename from packages/claviature/package.json rename to packages/widgets/package.json index 16e8f549..213ae526 100644 --- a/packages/claviature/package.json +++ b/packages/widgets/package.json @@ -1,7 +1,7 @@ { - "name": "@strudel/claviature", + "name": "@strudel/widgets", "version": "1.0.1", - "description": "Claviature component for Strudel", + "description": "Widget web components for Strudel", "main": "dist/index.mjs", "type": "module", "scripts": { diff --git a/packages/claviature/vite.config.js b/packages/widgets/vite.config.js similarity index 100% rename from packages/claviature/vite.config.js rename to packages/widgets/vite.config.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75c4e0a9..9ceaf3d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,31 +140,6 @@ importers: specifier: ^5.0.10 version: 5.0.10 - packages/claviature: - dependencies: - '@strudel/core': - specifier: workspace:* - version: link:../core - '@strudel/transpiler': - specifier: workspace:* - version: link:../transpiler - claviature: - specifier: ^0.1.0 - version: 0.1.0 - solid-element: - specifier: ^1.8.0 - version: 1.8.0(solid-js@1.8.15) - solid-js: - specifier: ^1.8.15 - version: 1.8.15 - vite-plugin-solid: - specifier: ^2.10.1 - version: 2.10.1(solid-js@1.8.15)(vite@5.0.11) - devDependencies: - vite: - specifier: ^5.0.10 - version: 5.0.11(@types/node@20.10.6) - packages/codemirror: dependencies: '@codemirror/autocomplete': @@ -513,6 +488,31 @@ importers: specifier: ^5.0.10 version: 5.0.10 + packages/widgets: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + '@strudel/transpiler': + specifier: workspace:* + version: link:../transpiler + claviature: + specifier: ^0.1.0 + version: 0.1.0 + solid-element: + specifier: ^1.8.0 + version: 1.8.0(solid-js@1.8.15) + solid-js: + specifier: ^1.8.15 + version: 1.8.15 + vite-plugin-solid: + specifier: ^2.10.1 + version: 2.10.1(solid-js@1.8.15)(vite@5.0.11) + devDependencies: + vite: + specifier: ^5.0.10 + version: 5.0.11(@types/node@20.10.6) + packages/xen: dependencies: '@strudel/core': @@ -570,9 +570,6 @@ importers: '@nanostores/react': specifier: ^0.7.1 version: 0.7.1(nanostores@0.9.5)(react@18.2.0) - '@strudel/claviature': - specifier: workspace:* - version: link:../packages/claviature '@strudel/codemirror': specifier: workspace:* version: link:../packages/codemirror @@ -618,6 +615,9 @@ importers: '@strudel/webaudio': specifier: workspace:* version: link:../packages/webaudio + '@strudel/widgets': + specifier: workspace:* + version: link:../packages/widgets '@strudel/xen': specifier: workspace:* version: link:../packages/xen @@ -854,7 +854,7 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/gen-mapping': 0.1.1 - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.20 /@apideck/better-ajv-errors@0.3.6(ajv@8.12.0): resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} @@ -3096,7 +3096,6 @@ packages: /@jridgewell/resolve-uri@3.1.1: resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} engines: {node: '>=6.0.0'} - dev: true /@jridgewell/set-array@1.1.2: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} @@ -3126,7 +3125,6 @@ packages: dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 - dev: true /@jsdoc/salty@0.2.3: resolution: {integrity: sha512-bbtCxCkxcnWhi50I+4Lj6mdz9w3pOXOgEQrID8TCZ/DF51fW7M9GCQW2y45SpBDdHd1Eirm1X/Cf6CkAAe8HPg==} diff --git a/website/package.json b/website/package.json index e80bfa7a..3fd05d54 100644 --- a/website/package.json +++ b/website/package.json @@ -25,7 +25,7 @@ "@heroicons/react": "^2.1.1", "@nanostores/persistent": "^0.9.1", "@nanostores/react": "^0.7.1", - "@strudel/claviature": "workspace:*", + "@strudel/widgets": "workspace:*", "@strudel/codemirror": "workspace:*", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 31e518cb..1df5a5b5 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -82,7 +82,7 @@ export function loadModules() { import('@strudel/serial'), import('@strudel/soundfonts'), import('@strudel/csound'), - import('@strudel/claviature'), + import('@strudel/widgets'), ]; if (isTauri()) { modules = modules.concat([ From 094b30cd264dc61879fbe1d8f39db47e17741318 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Mar 2024 00:39:31 +0100 Subject: [PATCH 43/69] use hap color for claviature --- packages/widgets/Claviature.jsx | 4 ++-- packages/widgets/README.md | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/widgets/Claviature.jsx b/packages/widgets/Claviature.jsx index 0d78d242..e4ebfec6 100644 --- a/packages/widgets/Claviature.jsx +++ b/packages/widgets/Claviature.jsx @@ -37,14 +37,14 @@ registerWidget('claviature', 'strudel-claviature'); Pattern.prototype.claviature = function (id, options = {}) { return this.onFrame((haps) => { - const keys = haps.map((h) => h.value.note); + const colorize = haps.map((h) => ({ keys: [h.value.note], color: h.context?.color || 'steelblue' })); let el = document.getElementById(id); el?.setAttribute( 'options', JSON.stringify({ ...options, range: options.range || ['A2', 'C6'], - colorize: [{ keys: keys, color: options.color || 'steelblue' }], + colorize, }), ); }); diff --git a/packages/widgets/README.md b/packages/widgets/README.md index a5c36193..53847c5d 100644 --- a/packages/widgets/README.md +++ b/packages/widgets/README.md @@ -19,9 +19,9 @@ Here is an example that uses all available options: ```js chord("").voicing().piano() +.color('cyan') .claviature({ range: ['C1', 'C6'], // rendered note range - color: 'yellow', // highlighting color palette: ['cyan', 'magenta'], stroke: 'black', scaleX: 1, scaleY: 1, @@ -29,5 +29,3 @@ chord("").voicing().piano() lowerHeight: 50 }) ``` - -Note: The `Pattern.claviature` method uses the `colorization` option internally, so don't override that and use the `color` option for changing the highlighting color. From 48e0691eec3f0a5496258ce0bc52336a8ba01774 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Mar 2024 01:40:21 +0100 Subject: [PATCH 44/69] add pianoroll widget --- packages/codemirror/widget.mjs | 19 ++++++++++++------- packages/draw/draw.mjs | 4 ++-- packages/draw/pianoroll.mjs | 3 ++- packages/widgets/Claviature.jsx | 24 +++++++++++------------- packages/widgets/Pianoroll.jsx | 30 ++++++++++++++++++++++++++++++ packages/widgets/index.mjs | 1 + packages/widgets/package.json | 1 + pnpm-lock.yaml | 3 +++ 8 files changed, 62 insertions(+), 23 deletions(-) create mode 100644 packages/widgets/Pianoroll.jsx diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 3171d0df..c0bd9a24 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -14,13 +14,18 @@ export const updateWidgets = (view, widgets) => { }; function getWidgets(widgetConfigs) { - return widgetConfigs.map(({ to, type }) => { - return Decoration.widget({ - widget: new BlockWidget(to, type), - side: 0, - block: true, - }).range(to); - }); + return ( + widgetConfigs + // codemirror throws an error if we don't sort + .sort((a, b) => a.to - b.to) + .map(({ to, type }) => { + return Decoration.widget({ + widget: new BlockWidget(to, type), + side: 0, + block: true, + }).range(to); + }) + ); } const widgetField = StateField.define( diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 2ea531cf..63245baf 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -29,14 +29,14 @@ export const getDrawContext = (id = 'test-canvas', options) => { return canvas.getContext(contextType); }; -Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) { +Pattern.prototype.draw = function (callback, { from, to, onQuery, ctx } = {}) { if (typeof window === 'undefined') { return this; } if (window.strudelAnimation) { cancelAnimationFrame(window.strudelAnimation); } - const ctx = getDrawContext(); + ctx = ctx || getDrawContext(); let cycle, events = []; const animate = (time) => { diff --git a/packages/draw/pianoroll.mjs b/packages/draw/pianoroll.mjs index 74b1480e..b032ef1a 100644 --- a/packages/draw/pianoroll.mjs +++ b/packages/draw/pianoroll.mjs @@ -30,7 +30,7 @@ const getValue = (e) => { }; Pattern.prototype.pianoroll = function (options = {}) { - let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options; + let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false, ctx } = options; let from = -cycles * playhead; let to = cycles * (1 - playhead); @@ -49,6 +49,7 @@ Pattern.prototype.pianoroll = function (options = {}) { { from: from - overscan, to: to + overscan, + ctx, }, ); return this; diff --git a/packages/widgets/Claviature.jsx b/packages/widgets/Claviature.jsx index e4ebfec6..33777be4 100644 --- a/packages/widgets/Claviature.jsx +++ b/packages/widgets/Claviature.jsx @@ -17,19 +17,17 @@ customElement('strudel-claviature', { options: JSON.stringify(defaultOptions) }, return c; }; return ( -
- - - {(el) => { - return ( - - {el.value} - - ); - }} - - -
+ + + {(el) => { + return ( + + {el.value} + + ); + }} + + ); }); diff --git a/packages/widgets/Pianoroll.jsx b/packages/widgets/Pianoroll.jsx new file mode 100644 index 00000000..bf7c145c --- /dev/null +++ b/packages/widgets/Pianoroll.jsx @@ -0,0 +1,30 @@ +import { Pattern } from '@strudel/core'; +import { registerWidget } from '@strudel/transpiler'; +import { customElement } from 'solid-element'; + +customElement('strudel-pianoroll', { options: JSON.stringify('{}') }, (props, { element }) => { + return ; +}); + +registerWidget('roll', 'strudel-pianoroll'); + +Pattern.prototype.roll = function (id, options = { fold: 1 }) { + // TODO: remove setTimeout... + setTimeout(() => { + let el = document.getElementById(id); + if (!el) { + console.log('widget not found...'); + return this; + } + const { width = 400, height = 100 } = options; + const canvas = el?.shadowRoot.firstChild; + const pixelRatio = window.devicePixelRatio; + canvas.width = width * pixelRatio; + canvas.height = height * pixelRatio; + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; + const ctx = canvas.getContext('2d'); + this.pianoroll({ ...options, ctx }); + }); + return this; +}; diff --git a/packages/widgets/index.mjs b/packages/widgets/index.mjs index 281c6ab8..80d01ab5 100644 --- a/packages/widgets/index.mjs +++ b/packages/widgets/index.mjs @@ -1 +1,2 @@ export * from './Claviature.jsx'; +export * from './Pianoroll.jsx'; diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 213ae526..c77c2712 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -29,6 +29,7 @@ "dependencies": { "@strudel/core": "workspace:*", "@strudel/transpiler": "workspace:*", + "@strudel/draw": "workspace:*", "claviature": "^0.1.0", "solid-element": "^1.8.0", "solid-js": "^1.8.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ceaf3d9..ee159ab2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -493,6 +493,9 @@ importers: '@strudel/core': specifier: workspace:* version: link:../core + '@strudel/draw': + specifier: workspace:* + version: link:../draw '@strudel/transpiler': specifier: workspace:* version: link:../transpiler From 83ea3bb138468aa4d4953c6dcaae3c5707f742ca Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Mar 2024 01:47:35 +0100 Subject: [PATCH 45/69] encapsulate canvas logic --- packages/widgets/Canvas.jsx | 21 +++++++++++++++++++++ packages/widgets/Pianoroll.jsx | 22 +++------------------- packages/widgets/index.mjs | 1 + 3 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 packages/widgets/Canvas.jsx diff --git a/packages/widgets/Canvas.jsx b/packages/widgets/Canvas.jsx new file mode 100644 index 00000000..9b6b668c --- /dev/null +++ b/packages/widgets/Canvas.jsx @@ -0,0 +1,21 @@ +import { customElement } from 'solid-element'; + +customElement('strudel-canvas', {}, () => { + return ; +}); + +export function getWidgetDrawContext(id, options) { + let el = document.getElementById(id); + if (!el) { + console.warn(`widget with id ${id} not found in the DOM`); + return; + } + const { width = 300, height = 100, pixelRatio = window.devicePixelRatio } = options || {}; + const canvas = el?.shadowRoot.firstChild; + canvas.width = width * pixelRatio; + canvas.height = height * pixelRatio; + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; + const ctx = canvas.getContext('2d'); + return ctx; +} diff --git a/packages/widgets/Pianoroll.jsx b/packages/widgets/Pianoroll.jsx index bf7c145c..f28444ca 100644 --- a/packages/widgets/Pianoroll.jsx +++ b/packages/widgets/Pianoroll.jsx @@ -1,29 +1,13 @@ import { Pattern } from '@strudel/core'; import { registerWidget } from '@strudel/transpiler'; -import { customElement } from 'solid-element'; +import { getWidgetDrawContext } from './Canvas.jsx'; -customElement('strudel-pianoroll', { options: JSON.stringify('{}') }, (props, { element }) => { - return ; -}); - -registerWidget('roll', 'strudel-pianoroll'); +registerWidget('roll', 'strudel-canvas'); Pattern.prototype.roll = function (id, options = { fold: 1 }) { // TODO: remove setTimeout... setTimeout(() => { - let el = document.getElementById(id); - if (!el) { - console.log('widget not found...'); - return this; - } - const { width = 400, height = 100 } = options; - const canvas = el?.shadowRoot.firstChild; - const pixelRatio = window.devicePixelRatio; - canvas.width = width * pixelRatio; - canvas.height = height * pixelRatio; - canvas.style.width = width + 'px'; - canvas.style.height = height + 'px'; - const ctx = canvas.getContext('2d'); + const ctx = getWidgetDrawContext(id, options); this.pianoroll({ ...options, ctx }); }); return this; diff --git a/packages/widgets/index.mjs b/packages/widgets/index.mjs index 80d01ab5..e42e43b1 100644 --- a/packages/widgets/index.mjs +++ b/packages/widgets/index.mjs @@ -1,2 +1,3 @@ export * from './Claviature.jsx'; export * from './Pianoroll.jsx'; +export * from './Canvas.jsx'; From a8712fd8ce5db32152af54de8cb014b8b24dcedf Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Mar 2024 09:48:04 +0100 Subject: [PATCH 46/69] remove this mess --- packages/core/ui.mjs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/core/ui.mjs b/packages/core/ui.mjs index 9343e062..86ceb286 100644 --- a/packages/core/ui.mjs +++ b/packages/core/ui.mjs @@ -4,19 +4,6 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { getTime } from './time.mjs'; - -function frame(callback) { - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); - } - const animate = (animationTime) => { - callback(animationTime, getTime()); - window.strudelAnimation = requestAnimationFrame(animate); - }; - requestAnimationFrame(animate); -} - export const backgroundImage = function (src, animateOptions = {}) { const container = document.getElementById('code'); const bg = 'background-image:url(' + src + ');background-size:contain;'; @@ -35,11 +22,6 @@ export const backgroundImage = function (src, animateOptions = {}) { if (funcOptions.length === 0) { return; } - frame((_, t) => - funcOptions.forEach(([option, value]) => { - handleOption(option, value(t)); - }), - ); }; export const cleanupUi = () => { From 6dce9d5deb0343852b3d09fbe78c4f7718b0d823 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Mar 2024 09:49:00 +0100 Subject: [PATCH 47/69] support multiple animationFrames + break out spiral draw logic --- packages/draw/draw.mjs | 21 +++++---- packages/draw/pianoroll.mjs | 3 +- packages/draw/spiral.mjs | 88 +++++++++++++++++++------------------ 3 files changed, 58 insertions(+), 54 deletions(-) diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 63245baf..910ac08c 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -29,12 +29,13 @@ export const getDrawContext = (id = 'test-canvas', options) => { return canvas.getContext(contextType); }; -Pattern.prototype.draw = function (callback, { from, to, onQuery, ctx } = {}) { +let animationFrames = {}; +Pattern.prototype.draw = function (callback, { id = 'std', from, to, onQuery, ctx } = {}) { if (typeof window === 'undefined') { return this; } - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); + if (animationFrames[id]) { + cancelAnimationFrame(animationFrames[id]); } ctx = ctx || getDrawContext(); let cycle, @@ -56,7 +57,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery, ctx } = {}) { } } callback(ctx, events, t, time); - window.strudelAnimation = requestAnimationFrame(animate); + animationFrames[id] = requestAnimationFrame(animate); }; requestAnimationFrame(animate); return this; @@ -64,18 +65,18 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery, ctx } = {}) { // this is a more generic helper to get a rendering callback for the currently active haps // TODO: this misses events that are prolonged with clip or duration (would need state) -Pattern.prototype.onFrame = function (fn, offset = 0) { +Pattern.prototype.onFrame = function (id, fn, offset = 0) { if (typeof window === 'undefined') { return this; } - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); + if (animationFrames[id]) { + cancelAnimationFrame(animationFrames[id]); } const animate = () => { const t = getTime() + offset; const haps = this.queryArc(t, t); fn(haps, t, this); - window.strudelAnimation = requestAnimationFrame(animate); + animationFrames[id] = requestAnimationFrame(animate); }; requestAnimationFrame(animate); return this; @@ -84,9 +85,7 @@ Pattern.prototype.onFrame = function (fn, offset = 0) { export const cleanupDraw = (clearScreen = true) => { const ctx = getDrawContext(); clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width); - if (window.strudelAnimation) { - cancelAnimationFrame(window.strudelAnimation); - } + Object.values(animationFrames).forEach((id) => cancelAnimationFrame(id)); if (window.strudelScheduler) { clearInterval(window.strudelScheduler); } diff --git a/packages/draw/pianoroll.mjs b/packages/draw/pianoroll.mjs index b032ef1a..29c61d18 100644 --- a/packages/draw/pianoroll.mjs +++ b/packages/draw/pianoroll.mjs @@ -30,7 +30,7 @@ const getValue = (e) => { }; Pattern.prototype.pianoroll = function (options = {}) { - let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false, ctx } = options; + let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false, ctx, id } = options; let from = -cycles * playhead; let to = cycles * (1 - playhead); @@ -50,6 +50,7 @@ Pattern.prototype.pianoroll = function (options = {}) { from: from - overscan, to: to + overscan, ctx, + id, }, ); return this; diff --git a/packages/draw/spiral.mjs b/packages/draw/spiral.mjs index 00bd62ec..4762e843 100644 --- a/packages/draw/spiral.mjs +++ b/packages/draw/spiral.mjs @@ -49,7 +49,7 @@ function spiralSegment(options) { ctx.stroke(); } -Pattern.prototype.spiral = function (options = {}) { +function drawSpiral(options) { const { stretch = 1, size = 80, @@ -65,54 +65,58 @@ Pattern.prototype.spiral = function (options = {}) { colorizeInactive = 0, fade = true, // logSpiral = true, + ctx, + time, + haps, + drawTime, } = options; - function spiral({ ctx, time, haps, drawTime }) { - const [w, h] = [ctx.canvas.width, ctx.canvas.height]; - ctx.clearRect(0, 0, w * 2, h * 2); - const [cx, cy] = [w / 2, h / 2]; - const settings = { - margin: size / stretch, - cx, - cy, - stretch, - cap, - thickness, - }; + const [w, h] = [ctx.canvas.width, ctx.canvas.height]; + ctx.clearRect(0, 0, w * 2, h * 2); + const [cx, cy] = [w / 2, h / 2]; + const settings = { + margin: size / stretch, + cx, + cy, + stretch, + cap, + thickness, + }; - const playhead = { - ...settings, - thickness: playheadThickness, - from: inset - playheadLength, - to: inset, - color: playheadColor, - }; + const playhead = { + ...settings, + thickness: playheadThickness, + from: inset - playheadLength, + to: inset, + color: playheadColor, + }; - const [min] = drawTime; - const rotate = steady * time; - haps.forEach((hap) => { - const isActive = hap.whole.begin <= time && hap.endClipped > time; - const from = hap.whole.begin - time + inset; - const to = hap.endClipped - time + inset - padding; - const { color } = hap.context; - const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1; - spiralSegment({ - ctx, - ...settings, - from, - to, - rotate, - color: colorizeInactive || isActive ? color : inactiveColor, - fromOpacity: opacity, - toOpacity: opacity, - }); - }); + const [min] = drawTime; + const rotate = steady * time; + haps.forEach((hap) => { + const isActive = hap.whole.begin <= time && hap.endClipped > time; + const from = hap.whole.begin - time + inset; + const to = hap.endClipped - time + inset - padding; + const { color } = hap.context; + const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1; spiralSegment({ ctx, - ...playhead, + ...settings, + from, + to, rotate, + color: colorizeInactive || isActive ? color : inactiveColor, + fromOpacity: opacity, + toOpacity: opacity, }); - } + }); + spiralSegment({ + ctx, + ...playhead, + rotate, + }); +} - return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime })); +Pattern.prototype.spiral = function (options = {}) { + return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options })); }; From 8742d50bad8edd6e43980c06f5324f5bb2207e0d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Mar 2024 09:50:17 +0100 Subject: [PATCH 48/69] simplify widget creation + fix bugs --- packages/codemirror/widget.mjs | 12 +++++++----- packages/transpiler/transpiler.mjs | 17 +++++------------ packages/widgets/Canvas.jsx | 21 --------------------- packages/widgets/Claviature.jsx | 14 ++++++-------- packages/widgets/Pianoroll.jsx | 14 -------------- packages/widgets/canvas.mjs | 23 +++++++++++++++++++++++ packages/widgets/index.mjs | 4 ++-- packages/widgets/package.json | 1 + packages/widgets/registry.mjs | 11 +++++++++++ pnpm-lock.yaml | 3 +++ 10 files changed, 58 insertions(+), 62 deletions(-) delete mode 100644 packages/widgets/Canvas.jsx delete mode 100644 packages/widgets/Pianoroll.jsx create mode 100644 packages/widgets/canvas.mjs create mode 100644 packages/widgets/registry.mjs diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index c0bd9a24..8499e28e 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -53,6 +53,12 @@ const widgetField = StateField.define( }, ); +const widgetElements = {}; +export function setWidget(id, el) { + widgetElements[id] = el; + el.id = id; +} + export class BlockWidget extends WidgetType { constructor(col, type) { super(); @@ -64,11 +70,7 @@ export class BlockWidget extends WidgetType { } toDOM() { const id = getWidgetID(this.col); // matches id generated in transpiler - let el = document.getElementById(id); - if (!el) { - el = document.createElement(this.type); - el.id = id; - } + const el = widgetElements[id]; return el; } ignoreEvent(e) { diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index db805e14..475b1e96 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -3,16 +3,9 @@ import { parse } from 'acorn'; import escodegen from 'escodegen'; import { walk } from 'estree-walker'; -let widgetComponents = {}; -// this function allows registering a pattern method name and component name -// e.g. register('claviature', 'strudel-claviature') -// .. will map the Pattern method 'claviature' to the web component 'strudel-claviature' -// the transpiler will turn .claviature(...args) into .claviature(id, ...args) -// the widgetPlugin of @strudel/codemirror will automatically create an instance of 'strudel-claviature' -// .. so you only have to implement the actual .claviature method (or what you've called it..) - -export function registerWidget(name, tagName) { - widgetComponents[name] = tagName; +let widgetMethods = []; +export function registerWidgetType(type) { + widgetMethods.push(type); } export function transpiler(input, options = {}) { @@ -63,7 +56,7 @@ export function transpiler(input, options = {}) { emitWidgets && widgets.push({ to: node.end, - type: widgetComponents[node.callee.property.name], + type: node.callee.property.name, }); return this.replace(widgetWithLocation(node)); } @@ -131,7 +124,7 @@ function isSliderFunction(node) { } function isWidgetMethod(node) { - return node.type === 'CallExpression' && Object.keys(widgetComponents).includes(node.callee.property?.name); + return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name); } function sliderWithLocation(node) { diff --git a/packages/widgets/Canvas.jsx b/packages/widgets/Canvas.jsx deleted file mode 100644 index 9b6b668c..00000000 --- a/packages/widgets/Canvas.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import { customElement } from 'solid-element'; - -customElement('strudel-canvas', {}, () => { - return ; -}); - -export function getWidgetDrawContext(id, options) { - let el = document.getElementById(id); - if (!el) { - console.warn(`widget with id ${id} not found in the DOM`); - return; - } - const { width = 300, height = 100, pixelRatio = window.devicePixelRatio } = options || {}; - const canvas = el?.shadowRoot.firstChild; - canvas.width = width * pixelRatio; - canvas.height = height * pixelRatio; - canvas.style.width = width + 'px'; - canvas.style.height = height + 'px'; - const ctx = canvas.getContext('2d'); - return ctx; -} diff --git a/packages/widgets/Claviature.jsx b/packages/widgets/Claviature.jsx index 33777be4..ed06e671 100644 --- a/packages/widgets/Claviature.jsx +++ b/packages/widgets/Claviature.jsx @@ -2,8 +2,7 @@ import { For } from 'solid-js'; import { customElement } from 'solid-element'; import { getClaviature } from 'claviature'; import { Dynamic } from 'solid-js/web'; -import { Pattern } from '@strudel/core'; -import { registerWidget } from '@strudel/transpiler'; +import { registerWidget } from './registry.mjs'; let defaultOptions = { range: ['A1', 'C6'], @@ -31,12 +30,11 @@ customElement('strudel-claviature', { options: JSON.stringify(defaultOptions) }, ); }); -registerWidget('claviature', 'strudel-claviature'); - -Pattern.prototype.claviature = function (id, options = {}) { - return this.onFrame((haps) => { +registerWidget('claviature', (id, options = {}, pat) => { + const el = document.getElementById(id) || document.createElement('strudel-claviature'); + setWidget(id, el); + return pat.onFrame(id, (haps) => { const colorize = haps.map((h) => ({ keys: [h.value.note], color: h.context?.color || 'steelblue' })); - let el = document.getElementById(id); el?.setAttribute( 'options', JSON.stringify({ @@ -46,4 +44,4 @@ Pattern.prototype.claviature = function (id, options = {}) { }), ); }); -}; +}); diff --git a/packages/widgets/Pianoroll.jsx b/packages/widgets/Pianoroll.jsx deleted file mode 100644 index f28444ca..00000000 --- a/packages/widgets/Pianoroll.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Pattern } from '@strudel/core'; -import { registerWidget } from '@strudel/transpiler'; -import { getWidgetDrawContext } from './Canvas.jsx'; - -registerWidget('roll', 'strudel-canvas'); - -Pattern.prototype.roll = function (id, options = { fold: 1 }) { - // TODO: remove setTimeout... - setTimeout(() => { - const ctx = getWidgetDrawContext(id, options); - this.pianoroll({ ...options, ctx }); - }); - return this; -}; diff --git a/packages/widgets/canvas.mjs b/packages/widgets/canvas.mjs new file mode 100644 index 00000000..e218cd76 --- /dev/null +++ b/packages/widgets/canvas.mjs @@ -0,0 +1,23 @@ +import { registerWidget } from './registry.mjs'; +import { setWidget } from '@strudel/codemirror'; + +function createCanvasWidget(id, options) { + const { width = 300, height = 100, pixelRatio = window.devicePixelRatio } = options || {}; + let canvas = document.getElementById(id) || document.createElement('canvas'); + canvas.width = width * pixelRatio; + canvas.height = height * pixelRatio; + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; + setWidget(id, canvas); + return canvas; +} + +registerWidget('roll', (id, options = { fold: 1 }, pat) => { + const ctx = createCanvasWidget(id, options).getContext('2d'); + return pat.pianoroll({ ...options, ctx, id }); +}); + +registerWidget('twist', (id, options = {}, pat) => { + const ctx = createCanvasWidget(id, options).getContext('2d'); + return pat.spiral({ ...options, ctx, size: 50, id }); +}); diff --git a/packages/widgets/index.mjs b/packages/widgets/index.mjs index e42e43b1..5ebfa67e 100644 --- a/packages/widgets/index.mjs +++ b/packages/widgets/index.mjs @@ -1,3 +1,3 @@ export * from './Claviature.jsx'; -export * from './Pianoroll.jsx'; -export * from './Canvas.jsx'; +export * from './canvas.mjs'; +export * from './registry.mjs'; diff --git a/packages/widgets/package.json b/packages/widgets/package.json index c77c2712..775e4c17 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -30,6 +30,7 @@ "@strudel/core": "workspace:*", "@strudel/transpiler": "workspace:*", "@strudel/draw": "workspace:*", + "@strudel/codemirror": "workspace:*", "claviature": "^0.1.0", "solid-element": "^1.8.0", "solid-js": "^1.8.15", diff --git a/packages/widgets/registry.mjs b/packages/widgets/registry.mjs new file mode 100644 index 00000000..0d1a3e8f --- /dev/null +++ b/packages/widgets/registry.mjs @@ -0,0 +1,11 @@ +import { registerWidgetType } from '@strudel/transpiler'; +import { Pattern } from '@strudel/core'; + +export function registerWidget(type, fn) { + registerWidgetType(type); + if (fn) { + Pattern.prototype[type] = function (id, options = { fold: 1 }) { + return fn(id, options, this); + }; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee159ab2..27da53e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -490,6 +490,9 @@ importers: packages/widgets: dependencies: + '@strudel/codemirror': + specifier: workspace:* + version: link:../codemirror '@strudel/core': specifier: workspace:* version: link:../core From 4a95bf67dad444d84b98370082daf52e32e224cf Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Mar 2024 10:00:07 +0100 Subject: [PATCH 49/69] move widget registry to codemirror package + add transpiler as dependency to codemirror --- packages/codemirror/package.json | 1 + packages/codemirror/widget.mjs | 14 ++++++++++++++ packages/widgets/Claviature.jsx | 2 +- packages/widgets/canvas.mjs | 9 ++++----- packages/widgets/index.mjs | 1 - packages/widgets/registry.mjs | 11 ----------- pnpm-lock.yaml | 3 +++ 7 files changed, 23 insertions(+), 18 deletions(-) delete mode 100644 packages/widgets/registry.mjs diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 0b39ebdd..133c4148 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -46,6 +46,7 @@ "@replit/codemirror-vscode-keymap": "^6.0.2", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", + "@strudel/transpiler": "workspace:*", "@uiw/codemirror-themes": "^4.21.21", "@uiw/codemirror-themes-all": "^4.21.21", "nanostores": "^0.9.5" diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 8499e28e..e2652327 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -1,5 +1,7 @@ import { StateEffect, StateField } from '@codemirror/state'; import { Decoration, EditorView, WidgetType } from '@codemirror/view'; +import { registerWidgetType } from '@strudel/transpiler'; +import { Pattern } from '@strudel/core'; const getWidgetID = (from) => `widget_${from}`; @@ -79,3 +81,15 @@ export class BlockWidget extends WidgetType { } export const widgetPlugin = [widgetField]; + +// widget implementer API to create a new widget type +export function registerWidget(type, fn) { + registerWidgetType(type); + if (fn) { + Pattern.prototype[type] = function (id, options = { fold: 1 }) { + // fn is expected to create a dom element and call setWidget(id, el); + // fn should also return the pattern + return fn(id, options, this); + }; + } +} diff --git a/packages/widgets/Claviature.jsx b/packages/widgets/Claviature.jsx index ed06e671..e4bda32b 100644 --- a/packages/widgets/Claviature.jsx +++ b/packages/widgets/Claviature.jsx @@ -2,7 +2,7 @@ import { For } from 'solid-js'; import { customElement } from 'solid-element'; import { getClaviature } from 'claviature'; import { Dynamic } from 'solid-js/web'; -import { registerWidget } from './registry.mjs'; +import { registerWidget } from '@strudel/codemirror'; let defaultOptions = { range: ['A1', 'C6'], diff --git a/packages/widgets/canvas.mjs b/packages/widgets/canvas.mjs index e218cd76..4dba01a0 100644 --- a/packages/widgets/canvas.mjs +++ b/packages/widgets/canvas.mjs @@ -1,7 +1,6 @@ -import { registerWidget } from './registry.mjs'; -import { setWidget } from '@strudel/codemirror'; +import { registerWidget, setWidget } from '@strudel/codemirror'; -function createCanvasWidget(id, options) { +function getCanvasWidget(id, options) { const { width = 300, height = 100, pixelRatio = window.devicePixelRatio } = options || {}; let canvas = document.getElementById(id) || document.createElement('canvas'); canvas.width = width * pixelRatio; @@ -13,11 +12,11 @@ function createCanvasWidget(id, options) { } registerWidget('roll', (id, options = { fold: 1 }, pat) => { - const ctx = createCanvasWidget(id, options).getContext('2d'); + const ctx = getCanvasWidget(id, options).getContext('2d'); return pat.pianoroll({ ...options, ctx, id }); }); registerWidget('twist', (id, options = {}, pat) => { - const ctx = createCanvasWidget(id, options).getContext('2d'); + const ctx = getCanvasWidget(id, options).getContext('2d'); return pat.spiral({ ...options, ctx, size: 50, id }); }); diff --git a/packages/widgets/index.mjs b/packages/widgets/index.mjs index 5ebfa67e..ad6e7d1f 100644 --- a/packages/widgets/index.mjs +++ b/packages/widgets/index.mjs @@ -1,3 +1,2 @@ export * from './Claviature.jsx'; export * from './canvas.mjs'; -export * from './registry.mjs'; diff --git a/packages/widgets/registry.mjs b/packages/widgets/registry.mjs deleted file mode 100644 index 0d1a3e8f..00000000 --- a/packages/widgets/registry.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { registerWidgetType } from '@strudel/transpiler'; -import { Pattern } from '@strudel/core'; - -export function registerWidget(type, fn) { - registerWidgetType(type); - if (fn) { - Pattern.prototype[type] = function (id, options = { fold: 1 }) { - return fn(id, options, this); - }; - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27da53e6..00608001 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,6 +184,9 @@ importers: '@strudel/draw': specifier: workspace:* version: link:../draw + '@strudel/transpiler': + specifier: workspace:* + version: link:../transpiler '@uiw/codemirror-themes': specifier: ^4.21.21 version: 4.21.21(@codemirror/language@6.10.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0) From dfd22b5e447bf3582eb0e1a22e96526b9f9ac3d0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Mar 2024 11:04:35 +0100 Subject: [PATCH 50/69] fix: claviature height + better defaults + break out solid web component creation --- packages/widgets/Claviature.jsx | 14 ++++++-------- packages/widgets/canvas.mjs | 11 ++++++----- packages/widgets/solid.mjs | 13 +++++++++++++ 3 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 packages/widgets/solid.mjs diff --git a/packages/widgets/Claviature.jsx b/packages/widgets/Claviature.jsx index e4bda32b..5624280a 100644 --- a/packages/widgets/Claviature.jsx +++ b/packages/widgets/Claviature.jsx @@ -3,12 +3,9 @@ import { customElement } from 'solid-element'; import { getClaviature } from 'claviature'; import { Dynamic } from 'solid-js/web'; import { registerWidget } from '@strudel/codemirror'; +import { getSolidWidget } from './solid.mjs'; -let defaultOptions = { - range: ['A1', 'C6'], -}; - -customElement('strudel-claviature', { options: JSON.stringify(defaultOptions) }, (props, { element }) => { +customElement('strudel-claviature', { options: '{}' }, (props, { element }) => { let svg = () => { let c = getClaviature({ options: JSON.parse(props.options), @@ -31,11 +28,12 @@ customElement('strudel-claviature', { options: JSON.stringify(defaultOptions) }, }); registerWidget('claviature', (id, options = {}, pat) => { - const el = document.getElementById(id) || document.createElement('strudel-claviature'); - setWidget(id, el); + options = { range: ['A0', 'C6'], scaleY: 1, scaleY: 0.5, scaleX: 0.5, ...options }; + const height = (options.upperHeight + options.lowerHeight) * options.scaleY; + const el = getSolidWidget('strudel-claviature', id, { ...options, height }); return pat.onFrame(id, (haps) => { const colorize = haps.map((h) => ({ keys: [h.value.note], color: h.context?.color || 'steelblue' })); - el?.setAttribute( + el.setAttribute( 'options', JSON.stringify({ ...options, diff --git a/packages/widgets/canvas.mjs b/packages/widgets/canvas.mjs index 4dba01a0..5fd15356 100644 --- a/packages/widgets/canvas.mjs +++ b/packages/widgets/canvas.mjs @@ -1,7 +1,7 @@ import { registerWidget, setWidget } from '@strudel/codemirror'; -function getCanvasWidget(id, options) { - const { width = 300, height = 100, pixelRatio = window.devicePixelRatio } = options || {}; +function getCanvasWidget(id, options = {}) { + const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options; let canvas = document.getElementById(id) || document.createElement('canvas'); canvas.width = width * pixelRatio; canvas.height = height * pixelRatio; @@ -11,12 +11,13 @@ function getCanvasWidget(id, options) { return canvas; } -registerWidget('roll', (id, options = { fold: 1 }, pat) => { +registerWidget('roll', (id, options = {}, pat) => { const ctx = getCanvasWidget(id, options).getContext('2d'); - return pat.pianoroll({ ...options, ctx, id }); + return pat.pianoroll({ fold: 1, ...options, ctx, id }); }); registerWidget('twist', (id, options = {}, pat) => { + options = { width: 200, height: 200, size: 36, ...options }; const ctx = getCanvasWidget(id, options).getContext('2d'); - return pat.spiral({ ...options, ctx, size: 50, id }); + return pat.spiral({ ...options, ctx, id }); }); diff --git a/packages/widgets/solid.mjs b/packages/widgets/solid.mjs new file mode 100644 index 00000000..b8403249 --- /dev/null +++ b/packages/widgets/solid.mjs @@ -0,0 +1,13 @@ +import { setWidget } from '@strudel/codemirror'; + +export function getSolidWidget(type, id, options) { + let el = document.getElementById(id); + if (!el) { + el = document.createElement('div'); + const c = document.createElement(type); + el.appendChild(c); + } + el.height = options.height || 200; + setWidget(id, el); + return el?.firstChild; +} From 1043baf08a0da3acd89997f9ba71e8caff05c29e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Mar 2024 12:24:03 +0100 Subject: [PATCH 51/69] fix: catch errors in pianoroll getValue --- packages/draw/pianoroll.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/draw/pianoroll.mjs b/packages/draw/pianoroll.mjs index 29c61d18..2ec2d7ee 100644 --- a/packages/draw/pianoroll.mjs +++ b/packages/draw/pianoroll.mjs @@ -18,7 +18,13 @@ const getValue = (e) => { } note = note ?? n; if (typeof note === 'string') { - return noteToMidi(note); + try { + // TODO: n(run(32)).scale("D:minor") fails when trying to query negative time.. + return noteToMidi(note); + } catch (err) { + // console.warn(`error converting note to midi: ${err}`); // this spams to crazy + return 0; + } } if (typeof note === 'number') { return note; From d9d05e21c06ed1b599a99e2f6d6f0fe4017091ef Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 15 Mar 2024 12:24:11 +0100 Subject: [PATCH 52/69] full size piano by default --- packages/widgets/Claviature.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/widgets/Claviature.jsx b/packages/widgets/Claviature.jsx index 5624280a..dc216d56 100644 --- a/packages/widgets/Claviature.jsx +++ b/packages/widgets/Claviature.jsx @@ -28,7 +28,7 @@ customElement('strudel-claviature', { options: '{}' }, (props, { element }) => { }); registerWidget('claviature', (id, options = {}, pat) => { - options = { range: ['A0', 'C6'], scaleY: 1, scaleY: 0.5, scaleX: 0.5, ...options }; + options = { range: ['A0', 'C8'], scaleY: 1, scaleY: 0.5, scaleX: 0.5, ...options }; const height = (options.upperHeight + options.lowerHeight) * options.scaleY; const el = getSolidWidget('strudel-claviature', id, { ...options, height }); return pat.onFrame(id, (haps) => { From f4032dad22a5a8af595aabd796032c734dc321e5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 16 Mar 2024 02:14:24 +0100 Subject: [PATCH 53/69] inline multichannel scopes --- packages/superdough/superdough.mjs | 35 ++++++++++++++------------- packages/webaudio/scope.mjs | 38 ++++++++++++++++++------------ packages/widgets/canvas.mjs | 6 +++++ 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 3e6448d9..9aa8ee3c 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -215,35 +215,34 @@ function getReverb(orbit, duration, fade, lp, dim, ir) { return reverbs[orbit]; } -export let analyser, analyserData /* s = {} */; +export let analysers = {}, + analysersData = {}; -export function getAnalyser(/* orbit, */ fftSize = 2048) { - if (!analyser /*s [orbit] */) { +export function getAnalyserById(id, fftSize = 1024) { + if (!analysers[id]) { const analyserNode = getAudioContext().createAnalyser(); analyserNode.fftSize = fftSize; // getDestination().connect(analyserNode); - analyser /* s[orbit] */ = analyserNode; - //analyserData = new Uint8Array(analyser.frequencyBinCount); - analyserData = new Float32Array(analyser.frequencyBinCount); + analysers[id] = analyserNode; + analysersData[id] = new Float32Array(analysers[id].frequencyBinCount); } - if (analyser /* s[orbit] */.fftSize !== fftSize) { - analyser /* s[orbit] */.fftSize = fftSize; - //analyserData = new Uint8Array(analyser.frequencyBinCount); - analyserData = new Float32Array(analyser.frequencyBinCount); + if (analysers[id].fftSize !== fftSize) { + analysers[id].fftSize = fftSize; + analysersData[id] = new Float32Array(analysers[id].frequencyBinCount); } - return analyser /* s[orbit] */; + return analysers[id]; } -export function getAnalyzerData(type = 'time') { +export function getAnalyzerData(type = 'time', id = 1) { const getter = { - time: () => analyser?.getFloatTimeDomainData(analyserData), - frequency: () => analyser?.getFloatFrequencyData(analyserData), + time: () => analysers[id]?.getFloatTimeDomainData(analysersData[id]), + frequency: () => analyser[id]?.getFloatFrequencyData(analysersData[id]), }[type]; if (!getter) { throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`); } getter(); - return analyserData; + return analysersData[id]; } function effectSend(input, effect, wet) { @@ -256,6 +255,8 @@ function effectSend(input, effect, wet) { export function resetGlobalEffects() { delays = {}; reverbs = {}; + analysers = {}; + analysersData = {}; } export const superdough = async (value, deadline, hapDuration) => { @@ -512,8 +513,8 @@ export const superdough = async (value, deadline, hapDuration) => { // analyser let analyserSend; if (analyze) { - const analyserNode = getAnalyser(/* orbit, */ 2 ** (fft + 5)); - analyserSend = effectSend(post, analyserNode, analyze); + const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); + analyserSend = effectSend(post, analyserNode, 1); } // connect chain elements together diff --git a/packages/webaudio/scope.mjs b/packages/webaudio/scope.mjs index 0371366c..4337ec72 100644 --- a/packages/webaudio/scope.mjs +++ b/packages/webaudio/scope.mjs @@ -1,13 +1,21 @@ import { Pattern, clamp } from '@strudel/core'; import { getDrawContext } from '../draw/index.mjs'; -import { analyser, getAnalyzerData } from 'superdough'; +import { getAnalyzerData } from 'superdough'; export function drawTimeScope( analyser, - { align = true, color = 'white', thickness = 3, scale = 0.25, pos = 0.75, trigger = 0 } = {}, + { + align = true, + color = 'white', + thickness = 3, + scale = 0.25, + pos = 0.75, + trigger = 0, + ctx = getDrawContext(), + id = 1, + } = {}, ) { - const ctx = getDrawContext(); - const dataArray = getAnalyzerData('time'); + const dataArray = getAnalyzerData('time', id); ctx.lineWidth = thickness; ctx.strokeStyle = color; @@ -39,10 +47,9 @@ export function drawTimeScope( export function drawFrequencyScope( analyser, - { color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0 } = {}, + { color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0, ctx = getDrawContext(), id = 1 } = {}, ) { - const dataArray = getAnalyzerData('frequency'); - const ctx = getDrawContext(); + const dataArray = getAnalyzerData('frequency', id); const canvas = ctx.canvas; ctx.fillStyle = color; @@ -61,8 +68,7 @@ export function drawFrequencyScope( } } -function clearScreen(smear = 0, smearRGB = `0,0,0`) { - const ctx = getDrawContext(); +function clearScreen(smear = 0, smearRGB = `0,0,0`, ctx = getDrawContext()) { if (!smear) { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); } else { @@ -84,9 +90,10 @@ function clearScreen(smear = 0, smearRGB = `0,0,0`) { * s("sawtooth").fscope() */ Pattern.prototype.fscope = function (config = {}) { - return this.analyze(1).draw(() => { - clearScreen(config.smear); - analyser && drawFrequencyScope(analyser, config); + let id = config.id ?? 1; + return this.analyze(id).draw(() => { + clearScreen(config.smear, '0,0,0', config.ctx); + analysers[id] && drawFrequencyScope(analyser, config); }); }; @@ -105,9 +112,10 @@ Pattern.prototype.fscope = function (config = {}) { * s("sawtooth").scope() */ Pattern.prototype.tscope = function (config = {}) { - return this.analyze(1).draw(() => { - clearScreen(config.smear); - analyser && drawTimeScope(analyser, config); + let id = config.id ?? 1; + return this.analyze(id).draw(() => { + clearScreen(config.smear, '0,0,0', config.ctx); + analysers[id] && drawTimeScope(analysers[id], config); }); }; diff --git a/packages/widgets/canvas.mjs b/packages/widgets/canvas.mjs index 5fd15356..14aa8911 100644 --- a/packages/widgets/canvas.mjs +++ b/packages/widgets/canvas.mjs @@ -21,3 +21,9 @@ registerWidget('twist', (id, options = {}, pat) => { const ctx = getCanvasWidget(id, options).getContext('2d'); return pat.spiral({ ...options, ctx, id }); }); + +registerWidget('osci', (id, options = {}, pat) => { + options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options }; + const ctx = getCanvasWidget(id, options).getContext('2d'); + return pat.scope({ ...options, ctx, id }); +}); From ef284e9d131a7421702523a5793e43b67a470b5b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 16 Mar 2024 02:16:50 +0100 Subject: [PATCH 54/69] fix: bugs catched by linter --- packages/superdough/superdough.mjs | 2 +- packages/webaudio/scope.mjs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 9aa8ee3c..d0c276b0 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -236,7 +236,7 @@ export function getAnalyserById(id, fftSize = 1024) { export function getAnalyzerData(type = 'time', id = 1) { const getter = { time: () => analysers[id]?.getFloatTimeDomainData(analysersData[id]), - frequency: () => analyser[id]?.getFloatFrequencyData(analysersData[id]), + frequency: () => analysers[id]?.getFloatFrequencyData(analysersData[id]), }[type]; if (!getter) { throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`); diff --git a/packages/webaudio/scope.mjs b/packages/webaudio/scope.mjs index 4337ec72..309b4ab7 100644 --- a/packages/webaudio/scope.mjs +++ b/packages/webaudio/scope.mjs @@ -1,6 +1,6 @@ import { Pattern, clamp } from '@strudel/core'; import { getDrawContext } from '../draw/index.mjs'; -import { getAnalyzerData } from 'superdough'; +import { analysers, getAnalyzerData } from 'superdough'; export function drawTimeScope( analyser, @@ -93,7 +93,7 @@ Pattern.prototype.fscope = function (config = {}) { let id = config.id ?? 1; return this.analyze(id).draw(() => { clearScreen(config.smear, '0,0,0', config.ctx); - analysers[id] && drawFrequencyScope(analyser, config); + analysers[id] && drawFrequencyScope(analysers[id], config); }); }; From dd78dc96060c9feb7eabd171afe95b563be831f3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 16 Mar 2024 02:42:26 +0100 Subject: [PATCH 55/69] fix: animation frame cleanup --- packages/draw/draw.mjs | 19 ++++++++++++------- packages/webaudio/scope.mjs | 22 ++++++++++++++-------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 910ac08c..4e84878f 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -30,13 +30,20 @@ export const getDrawContext = (id = 'test-canvas', options) => { }; let animationFrames = {}; +function stopAnimationFrame(id) { + if (animationFrames[id] !== undefined) { + cancelAnimationFrame(animationFrames[id]); + delete animationFrames[id]; + } +} +function stopAllAnimations() { + Object.keys(animationFrames).forEach((id) => stopAnimationFrame(id)); +} Pattern.prototype.draw = function (callback, { id = 'std', from, to, onQuery, ctx } = {}) { if (typeof window === 'undefined') { return this; } - if (animationFrames[id]) { - cancelAnimationFrame(animationFrames[id]); - } + stopAnimationFrame(id); ctx = ctx || getDrawContext(); let cycle, events = []; @@ -69,9 +76,7 @@ Pattern.prototype.onFrame = function (id, fn, offset = 0) { if (typeof window === 'undefined') { return this; } - if (animationFrames[id]) { - cancelAnimationFrame(animationFrames[id]); - } + stopAnimationFrame(id); const animate = () => { const t = getTime() + offset; const haps = this.queryArc(t, t); @@ -85,7 +90,7 @@ Pattern.prototype.onFrame = function (id, fn, offset = 0) { export const cleanupDraw = (clearScreen = true) => { const ctx = getDrawContext(); clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width); - Object.values(animationFrames).forEach((id) => cancelAnimationFrame(id)); + stopAllAnimations(); if (window.strudelScheduler) { clearInterval(window.strudelScheduler); } diff --git a/packages/webaudio/scope.mjs b/packages/webaudio/scope.mjs index 309b4ab7..e4b856f3 100644 --- a/packages/webaudio/scope.mjs +++ b/packages/webaudio/scope.mjs @@ -91,10 +91,13 @@ function clearScreen(smear = 0, smearRGB = `0,0,0`, ctx = getDrawContext()) { */ Pattern.prototype.fscope = function (config = {}) { let id = config.id ?? 1; - return this.analyze(id).draw(() => { - clearScreen(config.smear, '0,0,0', config.ctx); - analysers[id] && drawFrequencyScope(analysers[id], config); - }); + return this.analyze(id).draw( + () => { + clearScreen(config.smear, '0,0,0', config.ctx); + analysers[id] && drawFrequencyScope(analysers[id], config); + }, + { id }, + ); }; /** @@ -113,10 +116,13 @@ Pattern.prototype.fscope = function (config = {}) { */ Pattern.prototype.tscope = function (config = {}) { let id = config.id ?? 1; - return this.analyze(id).draw(() => { - clearScreen(config.smear, '0,0,0', config.ctx); - analysers[id] && drawTimeScope(analysers[id], config); - }); + return this.analyze(id).draw( + () => { + clearScreen(config.smear, '0,0,0', config.ctx); + analysers[id] && drawTimeScope(analysers[id], config); + }, + { id }, + ); }; Pattern.prototype.scope = Pattern.prototype.tscope; From cdc200e1da2f6ff417ff5d2aafcf8f5ef9b7f187 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 16 Mar 2024 03:12:00 +0100 Subject: [PATCH 56/69] draw straight line when no analyser is defined yet + add todo for memory leak --- packages/webaudio/scope.mjs | 26 ++++++++++++++++++++++---- packages/widgets/canvas.mjs | 6 ++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/webaudio/scope.mjs b/packages/webaudio/scope.mjs index e4b856f3..c9ee1f33 100644 --- a/packages/webaudio/scope.mjs +++ b/packages/webaudio/scope.mjs @@ -15,13 +15,23 @@ export function drawTimeScope( id = 1, } = {}, ) { - const dataArray = getAnalyzerData('time', id); - ctx.lineWidth = thickness; ctx.strokeStyle = color; + let canvas = ctx.canvas; + + if (!analyser) { + // if analyser is undefined, draw straight line + // it may be undefined when no sound has been played yet + ctx.beginPath(); + let y = pos * canvas.height; + ctx.moveTo(0, y); + ctx.lineTo(canvas.width, y); + ctx.stroke(); + return; + } + const dataArray = getAnalyzerData('time', id); ctx.beginPath(); - let canvas = ctx.canvas; const bufferSize = analyser.frequencyBinCount; let triggerIndex = align @@ -49,6 +59,14 @@ export function drawFrequencyScope( analyser, { color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0, ctx = getDrawContext(), id = 1 } = {}, ) { + if (!analyser) { + ctx.beginPath(); + let y = pos * canvas.height; + ctx.moveTo(0, y); + ctx.lineTo(canvas.width, y); + ctx.stroke(); + return; + } const dataArray = getAnalyzerData('frequency', id); const canvas = ctx.canvas; @@ -119,7 +137,7 @@ Pattern.prototype.tscope = function (config = {}) { return this.analyze(id).draw( () => { clearScreen(config.smear, '0,0,0', config.ctx); - analysers[id] && drawTimeScope(analysers[id], config); + drawTimeScope(analysers[id], config); }, { id }, ); diff --git a/packages/widgets/canvas.mjs b/packages/widgets/canvas.mjs index 14aa8911..d5a705b9 100644 --- a/packages/widgets/canvas.mjs +++ b/packages/widgets/canvas.mjs @@ -25,5 +25,11 @@ registerWidget('twist', (id, options = {}, pat) => { registerWidget('osci', (id, options = {}, pat) => { options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options }; const ctx = getCanvasWidget(id, options).getContext('2d'); + // TODO: find way to clear previous analysers to avoid memory leak + // .scope passes id to Pattern.analyze, which is picked up by superdough + // .. which calls getAnalyserById(analyze), creating a new analyzer (+buffer) for that key + // the id here is the col number where the osci function ends (as passed by the transpiler) + // effectively, this means for each evaluation of .osci on a unique col, a new analyser will be created + // the problem is that the old ones will never get deleted.. this might pile up some memory return pat.scope({ ...options, ctx, id }); }); From 398533877cd76823fe98d3ca92acfaf77d6b626d Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Sat, 16 Mar 2024 17:24:37 +0000 Subject: [PATCH 57/69] Beat-oriented functionality (#976) * annotate pure values with their value, allowing single mininotation values to maintain their labels as pure * Don't use any 'patternified' arguments if they're all 'pure' * allow pattern weights (roughly, beats-per-cycle) to be inferred where possible, including from mininotation and across many transformations (e.g. `fast` with a 'pure' factor) * Add `beatCat`, similar to `timeCat` but funkier * `silence` has a weight of 1, add alternative `nothing` with a weight of 0, and `gap` function with weight argument * preserve weight across applicative operations (weight comes with the structure) * add `stack` alternatives that take advantage of pattern weights to align patterns differently - `stackLeft`, `stackRight`, `stackCentre`, `stackExpand`, with `stackBy` with an argument for patterning the alignment. --- packages/core/fraction.mjs | 9 + packages/core/pattern.mjs | 489 +++-- packages/core/test/controls.test.mjs | 12 +- packages/core/test/pattern.test.mjs | 24 +- packages/core/util.mjs | 20 + packages/mini/mini.mjs | 19 +- test/__snapshots__/examples.test.mjs.snap | 224 ++- test/__snapshots__/tunes.test.mjs.snap | 1975 +++++++++------------ 8 files changed, 1391 insertions(+), 1381 deletions(-) diff --git a/packages/core/fraction.mjs b/packages/core/fraction.mjs index 3af73e0e..43dc84ef 100644 --- a/packages/core/fraction.mjs +++ b/packages/core/fraction.mjs @@ -51,6 +51,11 @@ Fraction.prototype.max = function (other) { return this.gt(other) ? this : other; }; +Fraction.prototype.maximum = function (...others) { + others = others.map((x) => new Fraction(x)); + return others.reduce((max, other) => other.max(max), this); +}; + Fraction.prototype.min = function (other) { return this.lt(other) ? this : other; }; @@ -83,6 +88,10 @@ export const gcd = (...fractions) => { return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1)); }; +export const lcm = (...fractions) => { + return fractions.reduce((lcm, fraction) => lcm.lcm(fraction), fraction(1)); +}; + fraction._original = Fraction; export default fraction; diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 55a0f567..a86290fd 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import TimeSpan from './timespan.mjs'; -import Fraction from './fraction.mjs'; +import Fraction, { lcm } from './fraction.mjs'; import Hap from './hap.mjs'; import State from './state.mjs'; import { unionWithObj } from './value.mjs'; @@ -29,9 +29,23 @@ export class Pattern { * @param {function} query - The function that maps a `State` to an array of `Hap`. * @noAutocomplete */ - constructor(query) { + constructor(query, weight = undefined) { this.query = query; this._Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern + this.__weight = weight; // in terms of number of beats per cycle + } + + get weight() { + return this.__weight ?? Fraction(1); + } + + set weight(weight) { + this.__weight = Fraction(weight); + } + + setWeight(weight) { + this.weight = weight; + return this; } ////////////////////////////////////////////////////////////////////// @@ -47,7 +61,9 @@ export class Pattern { * "0 1 2".withValue(v => v + 10).log() */ withValue(func) { - return new Pattern((state) => this.query(state).map((hap) => hap.withValue(func))); + const result = new Pattern((state) => this.query(state).map((hap) => hap.withValue(func))); + result.weight = this.weight; + return result; } /** @@ -104,6 +120,8 @@ export class Pattern { * @returns Pattern */ appBoth(pat_val) { + const pat_func = this; + // Tidal's <*> const whole_func = function (span_a, span_b) { if (span_a == undefined || span_b == undefined) { @@ -111,7 +129,9 @@ export class Pattern { } return span_a.intersection_e(span_b); }; - return this.appWhole(whole_func, pat_val); + const result = pat_func.appWhole(whole_func, pat_val); + result.weight = lcm(pat_val.weight, pat_func.weight); + return result; } /** @@ -144,7 +164,9 @@ export class Pattern { } return haps; }; - return new Pattern(query); + const result = new Pattern(query); + result.weight = this.weight; + return result; } /** @@ -175,7 +197,9 @@ export class Pattern { } return haps; }; - return new Pattern(query); + const result = new Pattern(query); + result.weight = pat_val.weight; + return result; } bindWhole(choose_whole, func) { @@ -457,7 +481,11 @@ export class Pattern { * @noAutocomplete */ withContext(func) { - return this.withHap((hap) => hap.setContext(func(hap.context))); + const result = this.withHap((hap) => hap.setContext(func(hap.context))); + if (this.__pure !== undefined) { + result.__pure = this.__pure; + } + return result; } /** @@ -1123,13 +1151,25 @@ Pattern.prototype.factories = { // Elemental patterns +/** + * Does absolutely nothing, with a given metrical 'weight' + * @name gap + * @param {number} weight + * @example + * gap(3) // "~@3" + */ +export const gap = (weight) => new Pattern(() => [], Fraction(weight)); + /** * Does absolutely nothing.. * @name silence * @example * silence // "~" */ -export const silence = new Pattern(() => []); +export const silence = gap(1); + +/* Like silence, but with a 'weight' (relative duration) of 0 */ +export const nothing = gap(0); /** A discrete value that repeats once per cycle. * @@ -1142,7 +1182,9 @@ export function pure(value) { function query(state) { return state.span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value)); } - return new Pattern(query); + const result = new Pattern(query, 1); + result.__pure = value; + return result; } export function isPattern(thing) { @@ -1184,7 +1226,67 @@ export function stack(...pats) { // Array test here is to avoid infinite recursions.. pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); const query = (state) => flatten(pats.map((pat) => pat.query(state))); - return new Pattern(query); + const result = new Pattern(query); + result.weight = lcm(...pats.map((pat) => pat.weight)); + return result; +} + +function _stackWith(func, pats) { + pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); + if (pats.length === 0) { + return silence; + } + if (pats.length === 1) { + return pats[0]; + } + const [left, ...right] = pats.map((pat) => pat.weight); + const weight = left.maximum(...right); + return stack(...func(weight, pats)); +} + +export function stackLeft(...pats) { + return _stackWith( + (weight, pats) => pats.map((pat) => (pat.weight.eq(weight) ? pat : timeCat(pat, gap(weight.sub(pat.weight))))), + pats, + ); +} + +export function stackRight(...pats) { + return _stackWith( + (weight, pats) => pats.map((pat) => (pat.weight.eq(weight) ? pat : timeCat(gap(weight.sub(pat.weight)), pat))), + pats, + ); +} + +export function stackCentre(...pats) { + return _stackWith( + (weight, pats) => + pats.map((pat) => { + if (pat.weight.eq(weight)) { + return pat; + } + const g = gap(weight.sub(pat.weight).div(2)); + return timeCat(g, pat, g); + }), + pats, + ); +} + +export function stackBy(by, ...pats) { + const [left, ...right] = pats.map((pat) => pat.weight); + const weight = left.maximum(...right); + const lookup = { + centre: stackCentre, + left: stackLeft, + right: stackRight, + expand: stack, + beat: (...args) => polymeterSteps(weight, ...args), + }; + return by + .inhabit(lookup) + .fmap((func) => func(...pats)) + .innerJoin() + .setWeight(weight); } /** Concatenation: combines a list of patterns, switching between them successively, one per cycle: @@ -1198,7 +1300,11 @@ export function stack(...pats) { */ export function slowcat(...pats) { // Array test here is to avoid infinite recursions.. - pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); + pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat))); + + if (pats.length == 1) { + return pats[0]; + } const query = function (state) { const span = state.span; @@ -1245,13 +1351,29 @@ export function cat(...pats) { return slowcat(...pats); } -/** Like `seq`, but each step has a length, relative to the whole. +/** Sequences patterns like `seq`, but each pattern has a length, relative to the whole. + * This length can either be provided as a [length, pattern] pair, or inferred from + * mininotation as the number of toplevel steps. The latter only works if the mininotation + * hasn't first been modified by another function. * @return {Pattern} * @example * timeCat([3,"e3"],[1, "g3"]).note() - * // "e3@3 g3".note() + * // the same as "e3@3 g3".note() + * @example + * timeCat("bd sd cp","hh hh").sound() + * // the same as "bd sd cp hh hh".sound() */ export function timeCat(...timepats) { + // Weights may either be provided explicitly in [weight, pattern] pairs, or + // where possible, inferred from the pattern. + const findWeight = (x) => (Array.isArray(x) ? x : [x.weight, x]); + timepats = timepats.map(findWeight); + if (timepats.length == 1) { + const result = reify(timepats[0][1]); + result.weight = timepats[0][0]; + return result; + } + const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0)); let begin = Fraction(0); const pats = []; @@ -1260,7 +1382,9 @@ export function timeCat(...timepats) { pats.push(reify(pat)._compress(begin.div(total), end.div(total))); begin = end; } - return stack(...pats); + const result = stack(...pats); + result.weight = total; + return result; } /** @@ -1281,7 +1405,35 @@ export function arrange(...sections) { } export function fastcat(...pats) { - return slowcat(...pats)._fast(pats.length); + let result = slowcat(...pats); + if (pats.length > 1) { + result = result._fast(pats.length); + result.weight = pats.length; + } + return result; +} + +/** + * Concatenates patterns beatwise, similar to `timeCat`, but if an argument is a list, the whole pattern will be repeated for each element in the list. + * + * @return {Pattern} + * @example + * beatCat(["bd cp", "mt"], "bd").sound() + */ +export function beatCat(...groups) { + groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)])); + + const cycles = lcm(...groups.map((x) => Fraction(x.length))); + + let result = []; + for (let cycle = 0; cycle < cycles; ++cycle) { + result.push(...groups.map((x) => (x.length == 0 ? silence : x[cycle % x.length]))); + } + result = result.filter((x) => x.weight > 0); + const weight = result.reduce((a, b) => a.add(b.weight), Fraction(0)); + result = timeCat(...result); + result.weight = weight; + return result; } /** See `fastcat` */ @@ -1312,18 +1464,18 @@ function _sequenceCount(x) { } return [reify(x), 1]; } + /** - * Aligns one or more given sequences to the given number of steps per cycle. - * - * @name polymeterSteps - * @param {number} steps how many items are placed in one cycle - * @param {any[]} sequences one or more arrays of Patterns / values + * Speeds a pattern up or down, to fit to the given metrical 'weight'. * @example - * polymeterSteps(4, ["c", "d", "e"]) - * .note().stack(s("bd")) - * // note("{c d e}%4").stack(s("bd")) + * s("bd sd cp").reweight(4) + * // The same as s("{bd sd cp}%4") */ -export function polymeterSteps(steps, ...args) { +export const reweight = register('reweight', function (targetWeight, pat) { + return pat.fast(Fraction(targetWeight).div(pat.weight)); +}); + +export function _polymeterListSteps(steps, ...args) { const seqs = args.map((a) => _sequenceCount(a)); if (seqs.length == 0) { return silence; @@ -1346,15 +1498,51 @@ export function polymeterSteps(steps, ...args) { } /** - * Combines the given lists of patterns with the same pulse. This will create so called polymeters when different sized sequences are used. + * Aligns one or more given patterns to the given number of steps per cycle. + * This relies on patterns having coherent number of steps per cycle, + * + * @name polymeterSteps + * @param {number} steps how many items are placed in one cycle + * @param {any[]} patterns one or more patterns + * @example + * // the same as "{c d, e f g}%4" + * polymeterSteps(4, "c d", "e f g") + */ +export function polymeterSteps(steps, ...args) { + if (args.length == 0) { + return silence; + } + if (Array.isArray(args[0])) { + // Support old behaviour + return _polymeterListSteps(steps, ...args); + } + + return polymeter(...args).reweight(steps); +} + +/** + * Combines the given lists of patterns with the same pulse, creating polymeters when different sized sequences are used. * @synonyms pm * @example - * polymeter(["c", "eb", "g"], ["c2", "g2"]).note() - * // "{c eb g, c2 g2}".note() + * // The same as "{c eb g, c2 g2}" + * polymeter("c eb g", "c2 g2") * */ export function polymeter(...args) { - return polymeterSteps(0, ...args); + if (Array.isArray(args[0])) { + // Support old behaviour + return _polymeterListSteps(0, ...args); + } + + if (args.length == 0) { + return silence; + } + const weight = args[0].weight; + const [head, ...tail] = args; + + const result = stack(head, ...tail.map((pat) => pat._slow(pat.weight.div(weight)))); + result.weight = weight; + return result; } export const mask = curry((a, b) => reify(b).mask(a)); @@ -1396,7 +1584,7 @@ export const func = curry((a, b) => reify(b).func(a)); * @noAutocomplete * */ -export function register(name, func, patternify = true) { +export function register(name, func, patternify = true, preserveWeight = false) { if (Array.isArray(name)) { const result = {}; for (const name_item of name) { @@ -1411,26 +1599,39 @@ export function register(name, func, patternify = true) { pfunc = function (...args) { args = args.map(reify); const pat = args[args.length - 1]; + let result; + if (arity === 1) { - return func(pat); + result = func(pat); + } else { + const firstArgs = args.slice(0, -1); + + if (firstArgs.every((arg) => arg.__pure != undefined)) { + const pureArgs = firstArgs.map((arg) => arg.__pure); + result = func(...pureArgs, pat); + } else { + const [left, ...right] = firstArgs; + + let mapFn = (...args) => { + return func(...args, pat); + }; + mapFn = curry(mapFn, null, arity - 1); + result = right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin(); + } } - const [left, ...right] = args.slice(0, -1); - let mapFn = (...args) => { - // make sure to call func with the correct argument count - // args.length is expected to be <= arity-1 - // so we set undefined args explicitly undefined - Array(arity - 1) - .fill() - .map((_, i) => args[i] ?? undefined); - return func(...args, pat); - }; - mapFn = curry(mapFn, null, arity - 1); - return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin(); + if (preserveWeight) { + result.weight = pat.weight; + } + return result; }; } else { pfunc = function (...args) { args = args.map(reify); - return func(...args); + const result = func(...args); + if (preserveWeight) { + result.weight = args[args.length - 1].weight; + } + return result; }; } @@ -1665,7 +1866,9 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun * s("bd ~ sd cp").ply("<1 2 3>") */ export const ply = register('ply', function (factor, pat) { - return pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin(); + const result = pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin(); + result.weight = pat.weight.mul(factor); + return result; }); /** @@ -1685,7 +1888,9 @@ export const { fast, density } = register(['fast', 'density'], function (factor, } factor = Fraction(factor); const fastQuery = pat.withQueryTime((t) => t.mul(factor)); - return fastQuery.withHapTime((t) => t.div(factor)); + const result = fastQuery.withHapTime((t) => t.div(factor)); + result.weight = factor.mul(pat.weight); + return result; }); /** @@ -1810,10 +2015,15 @@ export const cpm = register('cpm', function (cpm, pat) { * @example * "bd ~".stack("hh ~".early(.1)).s() */ -export const early = register('early', function (offset, pat) { - offset = Fraction(offset); - return pat.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset)); -}); +export const early = register( + 'early', + function (offset, pat) { + offset = Fraction(offset); + return pat.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset)); + }, + true, + true, +); /** * Nudge a pattern to start later in time. Equivalent of Tidal's ~> operator @@ -1825,10 +2035,15 @@ export const early = register('early', function (offset, pat) { * @example * "bd ~".stack("hh ~".late(.1)).s() */ -export const late = register('late', function (offset, pat) { - offset = Fraction(offset); - return pat._early(Fraction(0).sub(offset)); -}); +export const late = register( + 'late', + function (offset, pat) { + offset = Fraction(offset); + return pat._early(Fraction(0).sub(offset)); + }, + true, + true, +); /** * Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern: @@ -1857,14 +2072,19 @@ export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a * @example * s("lt ht mt cp, [hh oh]*2").linger("<1 .5 .25 .125>") */ -export const linger = register('linger', function (t, pat) { - if (t == 0) { - return silence; - } else if (t < 0) { - return pat._zoom(t.add(1), 1)._slow(t); - } - return pat._zoom(0, t)._slow(t); -}); +export const linger = register( + 'linger', + function (t, pat) { + if (t == 0) { + return silence; + } else if (t < 0) { + return pat._zoom(t.add(1), 1)._slow(t); + } + return pat._zoom(0, t)._slow(t); + }, + true, + true, +); /** * Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one. @@ -1873,7 +2093,7 @@ export const linger = register('linger', function (t, pat) { * note(saw.range(40,52).segment(24)) */ export const segment = register('segment', function (rate, pat) { - return pat.struct(pure(true)._fast(rate)); + return pat.struct(pure(true)._fast(rate)).setWeight(rate); }); /** @@ -1883,10 +2103,15 @@ export const segment = register('segment', function (rate, pat) { * @example * s("bd").struct("1 0 0 1 0 0 1 0".lastOf(4, invert)) */ -export const { invert, inv } = register(['invert', 'inv'], function (pat) { - // Swap true/false in a binary pattern - return pat.fmap((x) => !x); -}); +export const { invert, inv } = register( + ['invert', 'inv'], + function (pat) { + // Swap true/false in a binary pattern + return pat.fmap((x) => !x); + }, + true, + true, +); /** * Applies the given function whenever the given pattern is in a true state. @@ -1935,24 +2160,29 @@ export const brak = register('brak', function (pat) { * @example * note("c d e g").rev() */ -export const rev = register('rev', function (pat) { - const query = function (state) { - const span = state.span; - const cycle = span.begin.sam(); - const next_cycle = span.begin.nextSam(); - const reflect = function (to_reflect) { - const reflected = to_reflect.withTime((time) => cycle.add(next_cycle.sub(time))); - // [reflected.begin, reflected.end] = [reflected.end, reflected.begin] -- didn't work - const tmp = reflected.begin; - reflected.begin = reflected.end; - reflected.end = tmp; - return reflected; +export const rev = register( + 'rev', + function (pat) { + const query = function (state) { + const span = state.span; + const cycle = span.begin.sam(); + const next_cycle = span.begin.nextSam(); + const reflect = function (to_reflect) { + const reflected = to_reflect.withTime((time) => cycle.add(next_cycle.sub(time))); + // [reflected.begin, reflected.end] = [reflected.end, reflected.begin] -- didn't work + const tmp = reflected.begin; + reflected.begin = reflected.end; + reflected.end = tmp; + return reflected; + }; + const haps = pat.query(state.setSpan(reflect(span))); + return haps.map((hap) => hap.withSpan(reflect)); }; - const haps = pat.query(state.setSpan(reflect(span))); - return haps.map((hap) => hap.withSpan(reflect)); - }; - return new Pattern(query).splitQueries(); -}); + return new Pattern(query).splitQueries(); + }, + false, + true, +); /** Like press, but allows you to specify the amount by which each * event is shifted. pressBy(0.5) is the same as press, while @@ -1994,9 +2224,14 @@ Pattern.prototype.hush = function () { * @example * note("c d e g").palindrome() */ -export const palindrome = register('palindrome', function (pat) { - return pat.lastOf(2, rev); -}); +export const palindrome = register( + 'palindrome', + function (pat) { + return pat.lastOf(2, rev); + }, + true, + true, +); /** * Jux with adjustable stereo width. 0 = mono, 1 = full stereo. @@ -2014,9 +2249,9 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, return dflt; }; const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by })); - const right = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by })); + const right = func(pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by }))); - return stack(left, func(right)); + return stack(left, right).setWeight(lcm(left.weight, right.weight)); }); /** @@ -2097,9 +2332,14 @@ const _iter = function (times, pat, back = false) { ); }; -export const iter = register('iter', function (times, pat) { - return _iter(times, pat, false); -}); +export const iter = register( + 'iter', + function (times, pat) { + return _iter(times, pat, false); + }, + true, + true, +); /** * Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles @@ -2110,9 +2350,14 @@ export const iter = register('iter', function (times, pat) { * @example * note("0 1 2 3".scale('A minor')).iterBack(4) */ -export const { iterBack, iterback } = register(['iterBack', 'iterback'], function (times, pat) { - return _iter(times, pat, true); -}); +export const { iterBack, iterback } = register( + ['iterBack', 'iterback'], + function (times, pat) { + return _iter(times, pat, true); + }, + true, + true, +); /** * Repeats each cycle the given number of times. @@ -2122,11 +2367,14 @@ export const { iterBack, iterback } = register(['iterBack', 'iterback'], functio * @example * note(irand(12).add(34)).segment(4).repeatCycles(2).s("gm_acoustic_guitar_nylon") */ -const _repeatCycles = function (n, pat) { - return slowcat(...Array(n).fill(pat)); -}; - -const { repeatCycles } = register('repeatCycles', _repeatCycles); +export const { repeatCycles } = register( + 'repeatCycles', + function (n, pat) { + return slowcat(...Array(n).fill(pat)); + }, + true, + true, +); /** * Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle). @@ -2150,7 +2398,7 @@ const _chunk = function (n, func, pat, back = false, fast = false) { return pat.when(binary_pat, func); }; -const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) { +export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) { return _chunk(n, func, pat, false, false); }); @@ -2185,10 +2433,15 @@ export const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], fun }); // TODO - redefine elsewhere in terms of mask -export const bypass = register('bypass', function (on, pat) { - on = Boolean(parseInt(on)); - return on ? silence : pat; -}); +export const bypass = register( + 'bypass', + function (on, pat) { + on = Boolean(parseInt(on)); + return on ? silence : pat; + }, + true, + true, +); /** * Loops the pattern inside at `offset` for `cycles`. @@ -2245,7 +2498,7 @@ export const chop = register('chop', function (n, pat) { const func = function (o) { return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o))); }; - return pat.squeezeBind(func); + return pat.squeezeBind(func).setWeight(pat.weight.mul(n)); }); /** @@ -2297,17 +2550,19 @@ const _loopAt = function (factor, pat, cps = 0.5) { export const slice = register( 'slice', function (npat, ipat, opat) { - return npat.innerBind((n) => - ipat.outerBind((i) => - opat.outerBind((o) => { - // If it's not an object, assume it's a string and make it a 's' control parameter - o = o instanceof Object ? o : { s: o }; - const begin = Array.isArray(n) ? n[i] : i / n; - const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n; - return pure({ begin, end, _slices: n, ...o }); - }), - ), - ); + return npat + .innerBind((n) => + ipat.outerBind((i) => + opat.outerBind((o) => { + // If it's not an object, assume it's a string and make it a 's' control parameter + o = o instanceof Object ? o : { s: o }; + const begin = Array.isArray(n) ? n[i] : i / n; + const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n; + return pure({ begin, end, _slices: n, ...o }); + }), + ), + ) + .setWeight(ipat.weight); }, false, // turns off auto-patternification ); @@ -2338,7 +2593,7 @@ export const splice = register( ...v, })), ); - }); + }).setWeight(ipat.weight); }, false, // turns off auto-patternification ); diff --git a/packages/core/test/controls.test.mjs b/packages/core/test/controls.test.mjs index 69d63645..387e506f 100644 --- a/packages/core/test/controls.test.mjs +++ b/packages/core/test/controls.test.mjs @@ -4,9 +4,10 @@ Copyright (C) 2023 Strudel contributors - see . */ -import { s } from '../controls.mjs'; +import { s, pan } from '../controls.mjs'; import { mini } from '../../mini/mini.mjs'; import { describe, it, expect } from 'vitest'; +import Fraction from '../fraction.mjs'; describe('controls', () => { it('should support controls', () => { @@ -29,4 +30,13 @@ describe('controls', () => { expect(s(mini('bd').pan(1)).firstCycleValues).toEqual([{ s: 'bd', pan: 1 }]); expect(s(mini('bd:1').pan(1)).firstCycleValues).toEqual([{ s: 'bd', n: 1, pan: 1 }]); }); + it('preserves weight of the left pattern', () => { + expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).weight).toEqual(Fraction(3)); + }); + it('preserves weight of the right pattern for .out', () => { + expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).weight).toEqual(Fraction(4)); + }); + it('combines weight of the pattern for .mix as lcm', () => { + expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).weight).toEqual(Fraction(12)); + }); }); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 566038fd..c81f1a8a 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -604,7 +604,7 @@ describe('Pattern', () => { }); }); describe('polymeter()', () => { - it('Can layer up cycles, stepwise', () => { + it('Can layer up cycles, stepwise, with lists', () => { expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual( fastcat(pure('d'), pure('e'), pure('d')).firstCycle(), ); @@ -613,6 +613,9 @@ describe('Pattern', () => { stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(), ); }); + it('Can layer up cycles, stepwise, with weighted patterns', () => { + sameFirst(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b')); + }); }); describe('firstOf()', () => { @@ -1116,4 +1119,23 @@ describe('Pattern', () => { ); }); }); + describe('weight', () => { + it('Is correctly preserved/calculated through transformations', () => { + expect(sequence(0, 1, 2, 3).linger(4).weight).toStrictEqual(Fraction(4)); + expect(sequence(0, 1, 2, 3).iter(4).weight).toStrictEqual(Fraction(4)); + expect(sequence(0, 1, 2, 3).fast(4).weight).toStrictEqual(Fraction(16)); + expect(sequence(0, 1, 2, 3).hurry(4).weight).toStrictEqual(Fraction(16)); + expect(sequence(0, 1, 2, 3).rev().weight).toStrictEqual(Fraction(4)); + expect(sequence(1).segment(10).weight).toStrictEqual(Fraction(10)); + expect(sequence(1, 0, 1).invert().weight).toStrictEqual(Fraction(3)); + expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4).weight).toStrictEqual(Fraction(8)); + expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4).weight).toStrictEqual(Fraction(8)); + expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3)).weight).toStrictEqual( + Fraction(4), + ); + expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).weight).toStrictEqual( + Fraction(4), + ); + }); + }); }); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index ca3cfc12..2c7b3d71 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -323,3 +323,23 @@ export function objectMap(obj, fn) { } return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)])); } + +// Floating point versions, see Fraction for rational versions +// // greatest common divisor +// export const gcd = function (x, y, ...z) { +// if (!y && z.length > 0) { +// return gcd(x, ...z); +// } +// if (!y) { +// return x; +// } +// return gcd(y, x % y, ...z); +// }; + +// // lowest common multiple +// export const lcm = function (x, y, ...z) { +// if (z.length == 0) { +// return (x * y) / gcd(x, y); +// } +// return lcm((x * y) / gcd(x, y), ...z); +// }; diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index fdd3d3b1..c032cf12 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -92,16 +92,16 @@ export function patternifyAST(ast, code, onEnter, offset = 0) { return strudel.stack(...children); } if (alignment === 'polymeter_slowcat') { - const aligned = children.map((child) => child._slow(strudel.Fraction(child.__weight ?? 1))); + const aligned = children.map((child) => child._slow(child.weight)); return strudel.stack(...aligned); } if (alignment === 'polymeter') { // polymeter const stepsPerCycle = ast.arguments_.stepsPerCycle ? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x)) - : strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1)); + : strudel.pure(strudel.Fraction(children.length > 0 ? children[0].weight : 1)); - const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1)))); + const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.weight)))); return strudel.stack(...aligned); } if (alignment === 'rand') { @@ -112,13 +112,18 @@ export function patternifyAST(ast, code, onEnter, offset = 0) { } const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); if (weightedChildren) { - const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0); - const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]])); - pat.__weight = weightSum; + const weightSum = ast.source_.reduce( + (sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)), + strudel.Fraction(0), + ); + const pat = strudel.timeCat( + ...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]), + ); + pat.weight = weightSum; return pat; } const pat = strudel.sequence(...children); - pat.__weight = children.length; + pat.weight = children.length; return pat; } case 'element': { diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e9f46dab..3c96b6bd 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -567,10 +567,8 @@ exports[`runs examples > example "_euclidRot" example index 20 1`] = ` exports[`runs examples > example "accelerate" example index 0 1`] = ` [ - "[ (0/1 → 1/1) ⇝ 2/1 | s:sax accelerate:0 ]", - "[ 0/1 ⇜ (1/1 → 2/1) | s:sax accelerate:0 ]", - "[ (2/1 → 3/1) ⇝ 4/1 | s:sax accelerate:1 ]", - "[ 2/1 ⇜ (3/1 → 4/1) | s:sax accelerate:1 ]", + "[ 0/1 → 2/1 | s:sax accelerate:0 ]", + "[ 2/1 → 4/1 | s:sax accelerate:1 ]", ] `; @@ -956,6 +954,31 @@ exports[`runs examples > example "bank" example index 0 1`] = ` ] `; +exports[`runs examples > example "beatCat" example index 0 1`] = ` +[ + "[ 0/1 → 1/5 | s:bd ]", + "[ 1/5 → 2/5 | s:cp ]", + "[ 2/5 → 3/5 | s:bd ]", + "[ 3/5 → 4/5 | s:mt ]", + "[ 4/5 → 1/1 | s:bd ]", + "[ 1/1 → 6/5 | s:bd ]", + "[ 6/5 → 7/5 | s:cp ]", + "[ 7/5 → 8/5 | s:bd ]", + "[ 8/5 → 9/5 | s:mt ]", + "[ 9/5 → 2/1 | s:bd ]", + "[ 2/1 → 11/5 | s:bd ]", + "[ 11/5 → 12/5 | s:cp ]", + "[ 12/5 → 13/5 | s:bd ]", + "[ 13/5 → 14/5 | s:mt ]", + "[ 14/5 → 3/1 | s:bd ]", + "[ 3/1 → 16/5 | s:bd ]", + "[ 16/5 → 17/5 | s:cp ]", + "[ 17/5 → 18/5 | s:bd ]", + "[ 18/5 → 19/5 | s:mt ]", + "[ 19/5 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "begin" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:rave begin:0 ]", @@ -1652,8 +1675,7 @@ exports[`runs examples > example "cpm" example index 0 1`] = ` "[ 0/1 → 2/3 | s:bd ]", "[ 1/3 → 2/3 | s:hh ]", "[ 2/3 → 1/1 | s:hh ]", - "[ (2/3 → 1/1) ⇝ 4/3 | s:sd ]", - "[ 2/3 ⇜ (1/1 → 4/3) | s:sd ]", + "[ 2/3 → 4/3 | s:sd ]", "[ 1/1 → 4/3 | s:hh ]", "[ 4/3 → 5/3 | s:hh ]", "[ 4/3 → 2/1 | s:bd ]", @@ -1662,8 +1684,7 @@ exports[`runs examples > example "cpm" example index 0 1`] = ` "[ 2/1 → 8/3 | s:sd ]", "[ 7/3 → 8/3 | s:hh ]", "[ 8/3 → 3/1 | s:hh ]", - "[ (8/3 → 3/1) ⇝ 10/3 | s:bd ]", - "[ 8/3 ⇜ (3/1 → 10/3) | s:bd ]", + "[ 8/3 → 10/3 | s:bd ]", "[ 3/1 → 10/3 | s:hh ]", "[ 10/3 → 11/3 | s:hh ]", "[ 10/3 → 4/1 | s:sd ]", @@ -2116,14 +2137,11 @@ exports[`runs examples > example "early" example index 0 1`] = ` [ "[ -1/10 ⇜ (0/1 → 2/5) | s:hh ]", "[ 0/1 → 1/2 | s:bd ]", - "[ (9/10 → 1/1) ⇝ 7/5 | s:hh ]", - "[ 9/10 ⇜ (1/1 → 7/5) | s:hh ]", + "[ 9/10 → 7/5 | s:hh ]", "[ 1/1 → 3/2 | s:bd ]", - "[ (19/10 → 2/1) ⇝ 12/5 | s:hh ]", - "[ 19/10 ⇜ (2/1 → 12/5) | s:hh ]", + "[ 19/10 → 12/5 | s:hh ]", "[ 2/1 → 5/2 | s:bd ]", - "[ (29/10 → 3/1) ⇝ 17/5 | s:hh ]", - "[ 29/10 ⇜ (3/1 → 17/5) | s:hh ]", + "[ 29/10 → 17/5 | s:hh ]", "[ 3/1 → 7/2 | s:bd ]", "[ (39/10 → 4/1) ⇝ 22/5 | s:hh ]", ] @@ -2443,10 +2461,8 @@ exports[`runs examples > example "firstOf" example index 0 1`] = ` exports[`runs examples > example "fit" example index 0 1`] = ` [ - "[ (0/1 → 1/1) ⇝ 2/1 | s:rhodes speed:0.5 unit:c ]", - "[ 0/1 ⇜ (1/1 → 2/1) | s:rhodes speed:0.5 unit:c ]", - "[ (2/1 → 3/1) ⇝ 4/1 | s:rhodes speed:0.5 unit:c ]", - "[ 2/1 ⇜ (3/1 → 4/1) | s:rhodes speed:0.5 unit:c ]", + "[ 0/1 → 2/1 | s:rhodes speed:0.5 unit:c ]", + "[ 2/1 → 4/1 | s:rhodes speed:0.5 unit:c ]", ] `; @@ -2827,6 +2843,8 @@ exports[`runs examples > example "gain" example index 0 1`] = ` ] `; +exports[`runs examples > example "gap" example index 0 1`] = `[]`; + exports[`runs examples > example "hpattack" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]", @@ -3094,11 +3112,9 @@ exports[`runs examples > example "hpsustain" example index 0 1`] = ` exports[`runs examples > example "hurry" example index 0 1`] = ` [ "[ 0/1 → 3/4 | s:bd speed:1 ]", - "[ (3/4 → 1/1) ⇝ 3/2 | s:sd n:2 speed:1 ]", - "[ 3/4 ⇜ (1/1 → 3/2) | s:sd n:2 speed:1 ]", + "[ 3/4 → 3/2 | s:sd n:2 speed:1 ]", "[ 3/2 → 15/8 | s:bd speed:2 ]", - "[ (15/8 → 2/1) ⇝ 9/4 | s:sd n:2 speed:2 ]", - "[ 15/8 ⇜ (2/1 → 9/4) | s:sd n:2 speed:2 ]", + "[ 15/8 → 9/4 | s:sd n:2 speed:2 ]", "[ 9/4 → 21/8 | s:bd speed:2 ]", "[ 21/8 → 3/1 | s:sd n:2 speed:2 ]", "[ 3/1 → 51/16 | s:bd speed:4 ]", @@ -3705,10 +3721,8 @@ exports[`runs examples > example "loop" example index 0 1`] = ` exports[`runs examples > example "loopAt" example index 0 1`] = ` [ - "[ (0/1 → 1/1) ⇝ 2/1 | s:rhodes speed:0.25 unit:c ]", - "[ 0/1 ⇜ (1/1 → 2/1) | s:rhodes speed:0.25 unit:c ]", - "[ (2/1 → 3/1) ⇝ 4/1 | s:rhodes speed:0.25 unit:c ]", - "[ 2/1 ⇜ (3/1 → 4/1) | s:rhodes speed:0.25 unit:c ]", + "[ 0/1 → 2/1 | s:rhodes speed:0.25 unit:c ]", + "[ 2/1 → 4/1 | s:rhodes speed:0.25 unit:c ]", ] `; @@ -4178,10 +4192,8 @@ exports[`runs examples > example "never" example index 0 1`] = ` exports[`runs examples > example "noise" example index 0 1`] = ` [ - "[ (0/1 → 1/1) ⇝ 2/1 | s:white ]", - "[ 0/1 ⇜ (1/1 → 2/1) | s:white ]", - "[ (2/1 → 3/1) ⇝ 4/1 | s:pink ]", - "[ 2/1 ⇜ (3/1 → 4/1) | s:pink ]", + "[ 0/1 → 2/1 | s:white ]", + "[ 2/1 → 4/1 | s:pink ]", ] `; @@ -4903,55 +4915,67 @@ exports[`runs examples > example "ply" example index 0 1`] = ` exports[`runs examples > example "polymeter" example index 0 1`] = ` [ - "[ 0/1 → 1/3 | note:c ]", - "[ 0/1 → 1/3 | note:c2 ]", - "[ 1/3 → 2/3 | note:eb ]", - "[ 1/3 → 2/3 | note:g2 ]", - "[ 2/3 → 1/1 | note:g ]", - "[ 2/3 → 1/1 | note:c2 ]", - "[ 1/1 → 4/3 | note:c ]", - "[ 1/1 → 4/3 | note:g2 ]", - "[ 4/3 → 5/3 | note:eb ]", - "[ 4/3 → 5/3 | note:c2 ]", - "[ 5/3 → 2/1 | note:g ]", - "[ 5/3 → 2/1 | note:g2 ]", - "[ 2/1 → 7/3 | note:c ]", - "[ 2/1 → 7/3 | note:c2 ]", - "[ 7/3 → 8/3 | note:eb ]", - "[ 7/3 → 8/3 | note:g2 ]", - "[ 8/3 → 3/1 | note:g ]", - "[ 8/3 → 3/1 | note:c2 ]", - "[ 3/1 → 10/3 | note:c ]", - "[ 3/1 → 10/3 | note:g2 ]", - "[ 10/3 → 11/3 | note:eb ]", - "[ 10/3 → 11/3 | note:c2 ]", - "[ 11/3 → 4/1 | note:g ]", - "[ 11/3 → 4/1 | note:g2 ]", + "[ 0/1 → 1/3 | c ]", + "[ 0/1 → 1/3 | c2 ]", + "[ 1/3 → 2/3 | eb ]", + "[ 1/3 → 2/3 | g2 ]", + "[ 2/3 → 1/1 | g ]", + "[ 2/3 → 1/1 | c2 ]", + "[ 1/1 → 4/3 | c ]", + "[ 1/1 → 4/3 | g2 ]", + "[ 4/3 → 5/3 | eb ]", + "[ 4/3 → 5/3 | c2 ]", + "[ 5/3 → 2/1 | g ]", + "[ 5/3 → 2/1 | g2 ]", + "[ 2/1 → 7/3 | c ]", + "[ 2/1 → 7/3 | c2 ]", + "[ 7/3 → 8/3 | eb ]", + "[ 7/3 → 8/3 | g2 ]", + "[ 8/3 → 3/1 | g ]", + "[ 8/3 → 3/1 | c2 ]", + "[ 3/1 → 10/3 | c ]", + "[ 3/1 → 10/3 | g2 ]", + "[ 10/3 → 11/3 | eb ]", + "[ 10/3 → 11/3 | c2 ]", + "[ 11/3 → 4/1 | g ]", + "[ 11/3 → 4/1 | g2 ]", ] `; exports[`runs examples > example "polymeterSteps" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c ]", - "[ 0/1 → 1/1 | s:bd ]", - "[ 1/4 → 1/2 | note:d ]", - "[ 1/2 → 3/4 | note:e ]", - "[ 3/4 → 1/1 | note:c ]", - "[ 1/1 → 5/4 | note:d ]", - "[ 1/1 → 2/1 | s:bd ]", - "[ 5/4 → 3/2 | note:e ]", - "[ 3/2 → 7/4 | note:c ]", - "[ 7/4 → 2/1 | note:d ]", - "[ 2/1 → 9/4 | note:e ]", - "[ 2/1 → 3/1 | s:bd ]", - "[ 9/4 → 5/2 | note:c ]", - "[ 5/2 → 11/4 | note:d ]", - "[ 11/4 → 3/1 | note:e ]", - "[ 3/1 → 13/4 | note:c ]", - "[ 3/1 → 4/1 | s:bd ]", - "[ 13/4 → 7/2 | note:d ]", - "[ 7/2 → 15/4 | note:e ]", - "[ 15/4 → 4/1 | note:c ]", + "[ 0/1 → 1/4 | c ]", + "[ 0/1 → 1/4 | e ]", + "[ 1/4 → 1/2 | d ]", + "[ 1/4 → 1/2 | f ]", + "[ 1/2 → 3/4 | c ]", + "[ 1/2 → 3/4 | g ]", + "[ 3/4 → 1/1 | d ]", + "[ 3/4 → 1/1 | e ]", + "[ 1/1 → 5/4 | c ]", + "[ 1/1 → 5/4 | f ]", + "[ 5/4 → 3/2 | d ]", + "[ 5/4 → 3/2 | g ]", + "[ 3/2 → 7/4 | c ]", + "[ 3/2 → 7/4 | e ]", + "[ 7/4 → 2/1 | d ]", + "[ 7/4 → 2/1 | f ]", + "[ 2/1 → 9/4 | c ]", + "[ 2/1 → 9/4 | g ]", + "[ 9/4 → 5/2 | d ]", + "[ 9/4 → 5/2 | e ]", + "[ 5/2 → 11/4 | c ]", + "[ 5/2 → 11/4 | f ]", + "[ 11/4 → 3/1 | d ]", + "[ 11/4 → 3/1 | g ]", + "[ 3/1 → 13/4 | c ]", + "[ 3/1 → 13/4 | e ]", + "[ 13/4 → 7/2 | d ]", + "[ 13/4 → 7/2 | f ]", + "[ 7/2 → 15/4 | c ]", + "[ 7/2 → 15/4 | g ]", + "[ 15/4 → 4/1 | d ]", + "[ 15/4 → 4/1 | e ]", ] `; @@ -5501,6 +5525,27 @@ exports[`runs examples > example "rev" example index 0 1`] = ` ] `; +exports[`runs examples > example "reweight" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd ]", + "[ 1/4 → 1/2 | s:sd ]", + "[ 1/2 → 3/4 | s:cp ]", + "[ 3/4 → 1/1 | s:bd ]", + "[ 1/1 → 5/4 | s:sd ]", + "[ 5/4 → 3/2 | s:cp ]", + "[ 3/2 → 7/4 | s:bd ]", + "[ 7/4 → 2/1 | s:sd ]", + "[ 2/1 → 9/4 | s:cp ]", + "[ 9/4 → 5/2 | s:bd ]", + "[ 5/2 → 11/4 | s:sd ]", + "[ 11/4 → 3/1 | s:cp ]", + "[ 3/1 → 13/4 | s:bd ]", + "[ 13/4 → 7/2 | s:sd ]", + "[ 7/2 → 15/4 | s:cp ]", + "[ 15/4 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "ribbon" example index 0 1`] = ` [ "[ 0/1 → 1/2 | note:d ]", @@ -6428,8 +6473,7 @@ exports[`runs examples > example "slice" example index 0 1`] = ` "[ 3/4 → 27/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]", "[ 27/32 → 15/16 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]", "[ 15/16 → 63/64 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]", - "[ (63/64 → 1/1) ⇝ 33/32 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]", - "[ 63/64 ⇜ (1/1 → 33/32) | begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 63/64 → 33/32 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]", "[ 33/32 → 9/8 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]", "[ 9/8 → 75/64 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]", "[ 75/64 → 39/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]", @@ -6442,8 +6486,7 @@ exports[`runs examples > example "slice" example index 0 1`] = ` "[ 57/32 → 15/8 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]", "[ 15/8 → 123/64 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]", "[ 123/64 → 63/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]", - "[ (63/32 → 2/1) ⇝ 33/16 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]", - "[ 63/32 ⇜ (2/1 → 33/16) | begin:0.625 end:0.75 _slices:8 s:breaks165 ]", + "[ 63/32 → 33/16 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]", "[ 33/16 → 69/32 | begin:0.75 end:0.875 _slices:8 s:breaks165 ]", "[ 69/32 → 9/4 | begin:0.875 end:1 _slices:8 s:breaks165 ]", "[ 9/4 → 75/32 | begin:0.875 end:1 _slices:8 s:breaks165 ]", @@ -7089,6 +7132,31 @@ exports[`runs examples > example "timeCat" example index 0 1`] = ` ] `; +exports[`runs examples > example "timeCat" example index 1 1`] = ` +[ + "[ 0/1 → 1/5 | s:bd ]", + "[ 1/5 → 2/5 | s:sd ]", + "[ 2/5 → 3/5 | s:cp ]", + "[ 3/5 → 4/5 | s:hh ]", + "[ 4/5 → 1/1 | s:hh ]", + "[ 1/1 → 6/5 | s:bd ]", + "[ 6/5 → 7/5 | s:sd ]", + "[ 7/5 → 8/5 | s:cp ]", + "[ 8/5 → 9/5 | s:hh ]", + "[ 9/5 → 2/1 | s:hh ]", + "[ 2/1 → 11/5 | s:bd ]", + "[ 11/5 → 12/5 | s:sd ]", + "[ 12/5 → 13/5 | s:cp ]", + "[ 13/5 → 14/5 | s:hh ]", + "[ 14/5 → 3/1 | s:hh ]", + "[ 3/1 → 16/5 | s:bd ]", + "[ 16/5 → 17/5 | s:sd ]", + "[ 17/5 → 18/5 | s:cp ]", + "[ 18/5 → 19/5 | s:hh ]", + "[ 19/5 → 4/1 | s:hh ]", +] +`; + exports[`runs examples > example "transpose" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C2 ]", diff --git a/test/__snapshots__/tunes.test.mjs.snap b/test/__snapshots__/tunes.test.mjs.snap index e943462f..d929b84a 100644 --- a/test/__snapshots__/tunes.test.mjs.snap +++ b/test/__snapshots__/tunes.test.mjs.snap @@ -260,39 +260,39 @@ exports[`renders tunes > tune: barryHarris 1`] = ` exports[`renders tunes > tune: bassFuge 1`] = ` [ "[ 0/1 → 1/8 | note:A2 s:flbass n:0 gain:0.3 cutoff:2206.5338497506646 resonance:10 clip:1 ]", - "[ -7/4 ⇜ (0/1 → 1/4) | note:C4 s:flbass n:0 gain:0.3 cutoff:2312.732504596285 resonance:10 clip:1 ]", - "[ -7/4 ⇜ (0/1 → 1/4) | note:E4 s:flbass n:0 gain:0.3 cutoff:2312.732504596285 resonance:10 clip:1 ]", - "[ -3/2 ⇜ (0/1 → 1/4) ⇝ 1/2 | gain:0.3 note:A4 s:flbass n:0 cutoff:2522.789774516997 resonance:10 clip:1 ]", - "[ -3/2 ⇜ (0/1 → 1/4) ⇝ 1/2 | gain:0.3 note:C5 s:flbass n:0 cutoff:2522.789774516997 resonance:10 clip:1 ]", - "[ -5/4 ⇜ (0/1 → 1/4) ⇝ 3/4 | gain:0.15 note:A4 s:flbass n:0 cutoff:2727.5302177148174 resonance:10 clip:1 ]", - "[ -5/4 ⇜ (0/1 → 1/4) ⇝ 3/4 | gain:0.15 note:C5 s:flbass n:0 cutoff:2727.5302177148174 resonance:10 clip:1 ]", + "[ -7/4 ⇜ (0/1 → 1/4) | note:C4 s:flbass n:0 gain:0.3 cutoff:915.3693764684064 resonance:10 clip:1 ]", + "[ -7/4 ⇜ (0/1 → 1/4) | note:E4 s:flbass n:0 gain:0.3 cutoff:915.3693764684064 resonance:10 clip:1 ]", + "[ -3/2 ⇜ (0/1 → 1/4) ⇝ 1/2 | gain:0.3 note:A4 s:flbass n:0 cutoff:1275.6208956766397 resonance:10 clip:1 ]", + "[ -3/2 ⇜ (0/1 → 1/4) ⇝ 1/2 | gain:0.3 note:C5 s:flbass n:0 cutoff:1275.6208956766397 resonance:10 clip:1 ]", + "[ -5/4 ⇜ (0/1 → 1/4) ⇝ 3/4 | gain:0.15 note:A4 s:flbass n:0 cutoff:1677.2102254830027 resonance:10 clip:1 ]", + "[ -5/4 ⇜ (0/1 → 1/4) ⇝ 3/4 | gain:0.15 note:C5 s:flbass n:0 cutoff:1677.2102254830027 resonance:10 clip:1 ]", "[ 0/1 → 1/4 | note:A3 s:flbass n:0 gain:0.3 cutoff:2312.732504596285 resonance:10 clip:1 ]", - "[ -1/1 ⇜ (0/1 → 1/2) ⇝ 1/1 | gain:0.075 note:A4 s:flbass n:0 cutoff:2924.3791043233605 resonance:10 clip:1 ]", - "[ -1/1 ⇜ (0/1 → 1/2) ⇝ 1/1 | gain:0.075 note:C5 s:flbass n:0 cutoff:2924.3791043233605 resonance:10 clip:1 ]", + "[ -1/1 ⇜ (0/1 → 1/2) ⇝ 1/1 | gain:0.075 note:A4 s:flbass n:0 cutoff:2100 resonance:10 clip:1 ]", + "[ -1/1 ⇜ (0/1 → 1/2) ⇝ 1/1 | gain:0.075 note:C5 s:flbass n:0 cutoff:2100 resonance:10 clip:1 ]", "[ 0/1 → 1/2 | s:bd n:1 ]", - "[ -3/4 ⇜ (0/1 → 3/4) ⇝ 5/4 | gain:0.0375 note:A4 s:flbass n:0 cutoff:2924.3791043233605 resonance:10 clip:1 ]", - "[ -3/4 ⇜ (0/1 → 3/4) ⇝ 5/4 | gain:0.0375 note:C5 s:flbass n:0 cutoff:2924.3791043233605 resonance:10 clip:1 ]", - "[ -3/2 ⇜ (1/4 → 1/2) | gain:0.3 note:A4 s:flbass n:0 cutoff:2522.789774516997 resonance:10 clip:1 ]", - "[ -3/2 ⇜ (1/4 → 1/2) | gain:0.3 note:C5 s:flbass n:0 cutoff:2522.789774516997 resonance:10 clip:1 ]", - "[ -5/4 ⇜ (1/4 → 1/2) ⇝ 3/4 | gain:0.15 note:A4 s:flbass n:0 cutoff:2727.5302177148174 resonance:10 clip:1 ]", - "[ -5/4 ⇜ (1/4 → 1/2) ⇝ 3/4 | gain:0.15 note:C5 s:flbass n:0 cutoff:2727.5302177148174 resonance:10 clip:1 ]", + "[ -3/4 ⇜ (0/1 → 3/4) ⇝ 5/4 | gain:0.0375 note:A4 s:flbass n:0 cutoff:2522.789774516997 resonance:10 clip:1 ]", + "[ -3/4 ⇜ (0/1 → 3/4) ⇝ 5/4 | gain:0.0375 note:C5 s:flbass n:0 cutoff:2522.789774516997 resonance:10 clip:1 ]", + "[ -3/2 ⇜ (1/4 → 1/2) | gain:0.3 note:A4 s:flbass n:0 cutoff:1275.6208956766397 resonance:10 clip:1 ]", + "[ -3/2 ⇜ (1/4 → 1/2) | gain:0.3 note:C5 s:flbass n:0 cutoff:1275.6208956766397 resonance:10 clip:1 ]", + "[ -5/4 ⇜ (1/4 → 1/2) ⇝ 3/4 | gain:0.15 note:A4 s:flbass n:0 cutoff:1677.2102254830027 resonance:10 clip:1 ]", + "[ -5/4 ⇜ (1/4 → 1/2) ⇝ 3/4 | gain:0.15 note:C5 s:flbass n:0 cutoff:1677.2102254830027 resonance:10 clip:1 ]", "[ 1/4 → 1/2 | note:C4 s:flbass n:0 gain:0.3 cutoff:2727.5302177148174 resonance:10 clip:1 ]", "[ 1/4 → 1/2 | note:E4 s:flbass n:0 gain:0.3 cutoff:2727.5302177148174 resonance:10 clip:1 ]", "[ 1/4 → 1/2 | s:hh n:0 ]", "[ 3/8 → 1/2 | note:A2 s:flbass n:0 gain:0.3 cutoff:2827.098521493671 resonance:10 clip:1 ]", - "[ -5/4 ⇜ (1/2 → 3/4) | gain:0.15 note:A4 s:flbass n:0 cutoff:2727.5302177148174 resonance:10 clip:1 ]", - "[ -5/4 ⇜ (1/2 → 3/4) | gain:0.15 note:C5 s:flbass n:0 cutoff:2727.5302177148174 resonance:10 clip:1 ]", - "[ -1/1 ⇜ (1/2 → 3/4) ⇝ 1/1 | gain:0.075 note:A4 s:flbass n:0 cutoff:2924.3791043233605 resonance:10 clip:1 ]", - "[ -1/1 ⇜ (1/2 → 3/4) ⇝ 1/1 | gain:0.075 note:C5 s:flbass n:0 cutoff:2924.3791043233605 resonance:10 clip:1 ]", + "[ -5/4 ⇜ (1/2 → 3/4) | gain:0.15 note:A4 s:flbass n:0 cutoff:1677.2102254830027 resonance:10 clip:1 ]", + "[ -5/4 ⇜ (1/2 → 3/4) | gain:0.15 note:C5 s:flbass n:0 cutoff:1677.2102254830027 resonance:10 clip:1 ]", + "[ -1/1 ⇜ (1/2 → 3/4) ⇝ 1/1 | gain:0.075 note:A4 s:flbass n:0 cutoff:2100 resonance:10 clip:1 ]", + "[ -1/1 ⇜ (1/2 → 3/4) ⇝ 1/1 | gain:0.075 note:C5 s:flbass n:0 cutoff:2100 resonance:10 clip:1 ]", "[ 1/2 → 3/4 | gain:0.3 note:A4 s:flbass n:0 cutoff:3110.8609453791396 resonance:10 clip:1 ]", "[ 1/2 → 3/4 | gain:0.3 note:C5 s:flbass n:0 cutoff:3110.8609453791396 resonance:10 clip:1 ]", "[ 1/2 → 1/1 | s:bd n:1 ]", "[ 1/2 → 1/1 | s:sd n:0 ]", "[ 3/4 → 7/8 | note:A2 s:flbass n:0 gain:0.3 cutoff:3366.0584981088073 resonance:10 clip:1 ]", - "[ -1/1 ⇜ (3/4 → 1/1) | gain:0.075 note:A4 s:flbass n:0 cutoff:2924.3791043233605 resonance:10 clip:1 ]", - "[ -1/1 ⇜ (3/4 → 1/1) | gain:0.075 note:C5 s:flbass n:0 cutoff:2924.3791043233605 resonance:10 clip:1 ]", - "[ -3/4 ⇜ (3/4 → 1/1) ⇝ 5/4 | gain:0.0375 note:A4 s:flbass n:0 cutoff:2924.3791043233605 resonance:10 clip:1 ]", - "[ -3/4 ⇜ (3/4 → 1/1) ⇝ 5/4 | gain:0.0375 note:C5 s:flbass n:0 cutoff:2924.3791043233605 resonance:10 clip:1 ]", + "[ -1/1 ⇜ (3/4 → 1/1) | gain:0.075 note:A4 s:flbass n:0 cutoff:2100 resonance:10 clip:1 ]", + "[ -1/1 ⇜ (3/4 → 1/1) | gain:0.075 note:C5 s:flbass n:0 cutoff:2100 resonance:10 clip:1 ]", + "[ -3/4 ⇜ (3/4 → 1/1) ⇝ 5/4 | gain:0.0375 note:A4 s:flbass n:0 cutoff:2522.789774516997 resonance:10 clip:1 ]", + "[ -3/4 ⇜ (3/4 → 1/1) ⇝ 5/4 | gain:0.0375 note:C5 s:flbass n:0 cutoff:2522.789774516997 resonance:10 clip:1 ]", "[ 3/4 → 1/1 | note:A3 s:flbass n:0 gain:0.3 cutoff:3443.5028842544402 resonance:10 clip:1 ]", "[ 3/4 → 1/1 | gain:0.15 note:A4 s:flbass n:0 cutoff:3443.5028842544402 resonance:10 clip:1 ]", "[ 3/4 → 1/1 | gain:0.15 note:C5 s:flbass n:0 cutoff:3443.5028842544402 resonance:10 clip:1 ]", @@ -692,8 +692,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 65/2 → 33/1 | note:62 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 131/4 → 33/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 131/4 → 33/1 | note:35 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (131/4 → 33/1) ⇝ 133/4 | note:76 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 131/4 ⇜ (33/1 → 133/4) | note:76 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 131/4 → 133/4 | note:76 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 33/1 → 67/2 | s:bd gain:0.8 ]", "[ 33/1 → 67/2 | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 133/4 → 100/3 | note:57 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0 release:0.1 delay:0.4 delaytime:0.12 ]", @@ -723,8 +722,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 69/2 → 35/1 | note:62 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 139/4 → 35/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 139/4 → 35/1 | note:35 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (139/4 → 35/1) ⇝ 141/4 | note:76 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 139/4 ⇜ (35/1 → 141/4) | note:76 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 139/4 → 141/4 | note:76 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 35/1 → 71/2 | s:bd gain:0.8 ]", "[ 35/1 → 71/2 | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 141/4 → 71/2 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", @@ -754,8 +752,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 73/2 → 37/1 | note:61 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 147/4 → 37/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 147/4 → 37/1 | note:33 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (147/4 → 37/1) ⇝ 149/4 | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 147/4 ⇜ (37/1 → 149/4) | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 147/4 → 149/4 | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 37/1 → 75/2 | s:bd gain:0.8 ]", "[ 37/1 → 75/2 | note:73 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 149/4 → 112/3 | note:61 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0 release:0.1 delay:0.4 delaytime:0.12 ]", @@ -785,8 +782,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 77/2 → 39/1 | note:61 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 155/4 → 39/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 155/4 → 39/1 | note:33 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (155/4 → 39/1) ⇝ 157/4 | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 155/4 ⇜ (39/1 → 157/4) | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 155/4 → 157/4 | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 39/1 → 79/2 | s:bd gain:0.8 ]", "[ 39/1 → 79/2 | note:73 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 39/1 → 79/2 | note:33 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", @@ -817,8 +813,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 81/2 → 41/1 | note:59 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 163/4 → 41/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 163/4 → 41/1 | note:31 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (163/4 → 41/1) ⇝ 165/4 | note:73 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 163/4 ⇜ (41/1 → 165/4) | note:73 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 163/4 → 165/4 | note:73 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 41/1 → 83/2 | s:bd gain:0.8 ]", "[ 41/1 → 83/2 | note:71 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 165/4 → 124/3 | note:62 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0 release:0.1 delay:0.4 delaytime:0.12 ]", @@ -848,8 +843,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 85/2 → 43/1 | note:59 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 171/4 → 43/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 171/4 → 43/1 | note:31 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (171/4 → 43/1) ⇝ 173/4 | note:73 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 171/4 ⇜ (43/1 → 173/4) | note:73 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 171/4 → 173/4 | note:73 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 43/1 → 87/2 | s:bd gain:0.8 ]", "[ 43/1 → 87/2 | note:71 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 173/4 → 87/2 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", @@ -879,8 +873,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 89/2 → 45/1 | note:58 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 179/4 → 45/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 179/4 → 45/1 | note:30 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (179/4 → 45/1) ⇝ 181/4 | note:71 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 179/4 ⇜ (45/1 → 181/4) | note:71 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 179/4 → 181/4 | note:71 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 45/1 → 91/2 | s:bd gain:0.8 ]", "[ 45/1 → 91/2 | note:70 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 181/4 → 136/3 | note:58 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0 release:0.1 delay:0.4 delaytime:0.12 ]", @@ -910,8 +903,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 93/2 → 47/1 | note:58 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 187/4 → 47/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 187/4 → 47/1 | note:42 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (187/4 → 47/1) ⇝ 189/4 | note:71 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 187/4 ⇜ (47/1 → 189/4) | note:71 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 187/4 → 189/4 | note:71 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 47/1 → 95/2 | s:bd gain:0.8 ]", "[ 47/1 → 95/2 | note:70 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 47/1 → 95/2 | note:30 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", @@ -942,8 +934,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 97/2 → 49/1 | note:63 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 195/4 → 49/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 195/4 → 49/1 | note:36 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (195/4 → 49/1) ⇝ 197/4 | note:77 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 195/4 ⇜ (49/1 → 197/4) | note:77 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 195/4 → 197/4 | note:77 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 49/1 → 99/2 | s:bd gain:0.8 ]", "[ 49/1 → 99/2 | note:75 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 197/4 → 148/3 | note:58 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0 release:0.1 delay:0.4 delaytime:0.12 ]", @@ -973,8 +964,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 101/2 → 51/1 | note:63 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 203/4 → 51/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 203/4 → 51/1 | note:36 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (203/4 → 51/1) ⇝ 205/4 | note:77 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 203/4 ⇜ (51/1 → 205/4) | note:77 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 203/4 → 205/4 | note:77 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 51/1 → 103/2 | s:bd gain:0.8 ]", "[ 51/1 → 103/2 | note:75 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 205/4 → 103/2 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", @@ -1004,8 +994,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 105/2 → 53/1 | note:62 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 211/4 → 53/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 211/4 → 53/1 | note:34 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (211/4 → 53/1) ⇝ 213/4 | note:75 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 211/4 ⇜ (53/1 → 213/4) | note:75 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 211/4 → 213/4 | note:75 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 53/1 → 107/2 | s:bd gain:0.8 ]", "[ 53/1 → 107/2 | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 213/4 → 160/3 | note:62 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0 release:0.1 delay:0.4 delaytime:0.12 ]", @@ -1035,8 +1024,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 109/2 → 55/1 | note:62 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 219/4 → 55/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 219/4 → 55/1 | note:34 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (219/4 → 55/1) ⇝ 221/4 | note:75 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 219/4 ⇜ (55/1 → 221/4) | note:75 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 219/4 → 221/4 | note:75 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 55/1 → 111/2 | s:bd gain:0.8 ]", "[ 55/1 → 111/2 | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 55/1 → 111/2 | note:34 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", @@ -1065,8 +1053,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 113/2 → 57/1 | note:60 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 227/4 → 57/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 227/4 → 57/1 | note:32 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (227/4 → 57/1) ⇝ 229/4 | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 227/4 ⇜ (57/1 → 229/4) | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 227/4 → 229/4 | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 57/1 → 115/2 | note:72 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 229/4 → 172/3 | note:63 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0 release:0.1 delay:0.4 delaytime:0.12 ]", "[ 229/4 → 172/3 | note:67 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0 release:0.1 delay:0.4 delaytime:0.12 ]", @@ -1092,8 +1079,7 @@ exports[`renders tunes > tune: caverave 1`] = ` "[ 117/2 → 59/1 | note:60 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 235/4 → 59/1 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 235/4 → 59/1 | note:32 s:sawtooth attack:0.001 decay:0.2 sustain:1 cutoff:500 ]", - "[ (235/4 → 59/1) ⇝ 237/4 | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", - "[ 235/4 ⇜ (59/1 → 237/4) | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", + "[ 235/4 → 237/4 | note:74 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 59/1 → 119/2 | note:72 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", "[ 237/4 → 119/2 | s:hh delay:0.3 delayfeedback:0.5 delaytime:0.125 gain:0.4 ]", "[ 237/4 → 239/4 | note:63 s:sawtooth cutoff:1200 gain:0.5 attack:0 decay:0.16 sustain:0.3 release:0.1 ]", @@ -6205,25 +6191,25 @@ exports[`renders tunes > tune: festivalOfFingers 1`] = ` exports[`renders tunes > tune: festivalOfFingers3 1`] = ` [ - "[ -1/2 ⇜ (0/1 → 1/6) | gain:0.1250000728312878 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ -1/2 ⇜ (0/1 → 1/6) | gain:0.12500057914389073 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", "[ -1/6 ⇜ (0/1 → 1/6) | clip:1 gain:0.5 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ -1/6 ⇜ (0/1 → 1/6) | clip:1 gain:0.125 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", - "[ -1/3 ⇜ (0/1 → 1/3) | gain:0.16666743745500448 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", + "[ -1/3 ⇜ (0/1 → 1/3) | gain:0.16666666666666666 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", "[ 0/1 → 1/3 | clip:1 gain:1 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", "[ 0/1 → 1/3 | clip:1 gain:0.25 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ -3/2 ⇜ (0/1 → 1/2) | gain:0.2500038714714082 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", - "[ -3/2 ⇜ (0/1 → 1/2) | gain:0.2500038714714082 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ -3/2 ⇜ (0/1 → 1/2) | gain:0.2500038714714082 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", - "[ -1/2 ⇜ (0/1 → 1/2) | gain:0.2500038714714082 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", - "[ -1/2 ⇜ (0/1 → 1/2) | gain:0.1250019357357041 clip:1 note:E7 s:piano release:0.1 pan:0.712962962962963 ]", - "[ -1/6 ⇜ (0/1 → 1/2) | gain:0.2500038714714082 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", + "[ -3/2 ⇜ (0/1 → 1/2) | gain:0.250030297563536 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", + "[ -3/2 ⇜ (0/1 → 1/2) | gain:0.250030297563536 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ -3/2 ⇜ (0/1 → 1/2) | gain:0.250030297563536 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ -1/2 ⇜ (0/1 → 1/2) | gain:0.25 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ -1/2 ⇜ (0/1 → 1/2) | gain:0.125 clip:1 note:E7 s:piano release:0.1 pan:0.712962962962963 ]", + "[ -1/6 ⇜ (0/1 → 1/2) | gain:0.25000115618250673 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", "[ 0/1 → 2/3 | gain:0.5000182089760518 clip:1 note:D2 s:piano release:0.1 pan:0.42592592592592593 ]", - "[ -1/1 ⇜ (0/1 → 1/1) | gain:0.16668682833029574 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", - "[ -1/1 ⇜ (0/1 → 1/1) | gain:0.16668682833029574 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ -1/1 ⇜ (0/1 → 1/1) | gain:0.16668682833029574 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", - "[ -1/2 ⇜ (0/1 → 1/1) ⇝ 3/2 | gain:0.1250498192352193 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ -1/2 ⇜ (0/1 → 1/1) ⇝ 3/2 | gain:0.1250498192352193 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ -1/2 ⇜ (0/1 → 1/1) ⇝ 3/2 | gain:0.1250498192352193 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", + "[ -1/1 ⇜ (0/1 → 1/1) | gain:0.16666666666666666 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ -1/1 ⇜ (0/1 → 1/1) | gain:0.16666666666666666 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ -1/1 ⇜ (0/1 → 1/1) | gain:0.16666666666666666 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", + "[ -1/2 ⇜ (0/1 → 1/1) ⇝ 3/2 | gain:0.12501512124772182 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", + "[ -1/2 ⇜ (0/1 → 1/1) ⇝ 3/2 | gain:0.12501512124772182 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ -1/2 ⇜ (0/1 → 1/1) ⇝ 3/2 | gain:0.12501512124772182 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 0/1 → 1/1 | gain:0.5000604849908873 clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 0/1 → 1/1 | gain:0.16668682833029574 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", "[ (0/1 → 1/1) ⇝ 2/1 | gain:0.5004609890274139 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", @@ -6240,9 +6226,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ (1/2 → 1/1) ⇝ 7/6 | gain:0.25013557675466036 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", "[ (1/2 → 1/1) ⇝ 3/2 | gain:0.25023049451370694 clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]", "[ (1/2 → 1/1) ⇝ 3/2 | gain:0.12511524725685347 clip:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", - "[ (1/2 → 1/1) ⇝ 5/2 | gain:0.2504392251375784 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", - "[ (1/2 → 1/1) ⇝ 5/2 | gain:0.2504392251375784 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ (1/2 → 1/1) ⇝ 5/2 | gain:0.2504392251375784 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ (1/2 → 1/1) ⇝ 5/2 | gain:0.25074029392543207 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", + "[ (1/2 → 1/1) ⇝ 5/2 | gain:0.25074029392543207 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ (1/2 → 1/1) ⇝ 5/2 | gain:0.25074029392543207 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", "[ 2/3 → 1/1 | clip:1 gain:1 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", "[ 2/3 → 1/1 | clip:1 gain:0.25 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ (2/3 → 1/1) ⇝ 4/3 | gain:0.5004609890274139 clip:1 note:D2 s:piano release:0.1 pan:0.42592592592592593 ]", @@ -6255,9 +6241,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 2/3 ⇜ (1/1 → 4/3) | gain:0.5004609890274139 clip:1 note:D2 s:piano release:0.1 pan:0.42592592592592593 ]", "[ 1/1 → 4/3 | clip:1 gain:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 1/1 → 4/3 | clip:1 gain:0.25 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", - "[ -1/2 ⇜ (1/1 → 3/2) | gain:0.1250498192352193 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ -1/2 ⇜ (1/1 → 3/2) | gain:0.1250498192352193 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ -1/2 ⇜ (1/1 → 3/2) | gain:0.1250498192352193 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", + "[ -1/2 ⇜ (1/1 → 3/2) | gain:0.12501512124772182 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", + "[ -1/2 ⇜ (1/1 → 3/2) | gain:0.12501512124772182 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ -1/2 ⇜ (1/1 → 3/2) | gain:0.12501512124772182 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 1/2 ⇜ (1/1 → 3/2) | gain:0.25023049451370694 clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]", "[ 1/2 ⇜ (1/1 → 3/2) | gain:0.12511524725685347 clip:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ 5/6 ⇜ (1/1 → 3/2) | gain:0.1251800316700185 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", @@ -6265,14 +6251,14 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 0/1 ⇜ (1/1 → 2/1) | gain:0.5004609890274139 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", "[ 0/1 ⇜ (1/1 → 2/1) | gain:0.5004609890274139 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 0/1 ⇜ (1/1 → 2/1) | gain:0.5004609890274139 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 1/2 ⇜ (1/1 → 2/1) ⇝ 5/2 | gain:0.2504392251375784 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", - "[ 1/2 ⇜ (1/1 → 2/1) ⇝ 5/2 | gain:0.2504392251375784 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ 1/2 ⇜ (1/1 → 2/1) ⇝ 5/2 | gain:0.2504392251375784 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ 1/2 ⇜ (1/1 → 2/1) ⇝ 5/2 | gain:0.25074029392543207 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", + "[ 1/2 ⇜ (1/1 → 2/1) ⇝ 5/2 | gain:0.25074029392543207 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ 1/2 ⇜ (1/1 → 2/1) ⇝ 5/2 | gain:0.25074029392543207 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", "[ 1/1 → 2/1 | gain:0.5014805878508641 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", "[ 1/1 → 2/1 | gain:0.16716019595028803 clip:1 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", - "[ (1/1 → 2/1) ⇝ 3/1 | gain:0.16716019595028803 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", - "[ (1/1 → 2/1) ⇝ 3/1 | gain:0.16716019595028803 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ (1/1 → 2/1) ⇝ 3/1 | gain:0.16716019595028803 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", + "[ (1/1 → 2/1) ⇝ 3/1 | gain:0.16777864249203656 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (1/1 → 2/1) ⇝ 3/1 | gain:0.16777864249203656 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ (1/1 → 2/1) ⇝ 3/1 | gain:0.16777864249203656 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", "[ 7/6 → 3/2 | clip:1 gain:0.5 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 7/6 → 3/2 | clip:1 gain:0.125 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", "[ 7/6 → 11/6 | gain:0.25074029392543207 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", @@ -6281,37 +6267,37 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 4/3 → 2/1 | gain:0.5019971884845844 clip:1 note:D2 s:piano release:0.1 pan:0.42592592592592593 ]", "[ 3/2 → 11/6 | clip:1 gain:0.5 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", "[ 3/2 → 11/6 | clip:1 gain:0.125 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ (3/2 → 2/1) ⇝ 13/6 | gain:0.12557314160386537 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", - "[ (3/2 → 2/1) ⇝ 5/2 | gain:0.25114628320773075 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", - "[ (3/2 → 2/1) ⇝ 5/2 | gain:0.12557314160386537 clip:1 note:E7 s:piano release:0.1 pan:0.712962962962963 ]", - "[ (3/2 → 2/1) ⇝ 7/2 | gain:0.12557314160386537 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ (3/2 → 2/1) ⇝ 7/2 | gain:0.12557314160386537 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ (3/2 → 2/1) ⇝ 7/2 | gain:0.12557314160386537 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", + "[ (3/2 → 2/1) ⇝ 13/6 | gain:0.1256534205464579 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (3/2 → 2/1) ⇝ 5/2 | gain:0.25166796373805483 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (3/2 → 2/1) ⇝ 5/2 | gain:0.12583398186902742 clip:1 note:E7 s:piano release:0.1 pan:0.712962962962963 ]", + "[ (3/2 → 2/1) ⇝ 7/2 | gain:0.12654642086444556 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", + "[ (3/2 → 2/1) ⇝ 7/2 | gain:0.12654642086444556 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ (3/2 → 2/1) ⇝ 7/2 | gain:0.12654642086444556 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 5/3 → 2/1 | clip:1 gain:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 5/3 → 2/1 | clip:1 gain:0.25 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", - "[ (5/3 → 2/1) ⇝ 7/3 | gain:0.16753789406194386 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", + "[ (5/3 → 2/1) ⇝ 7/3 | gain:0.16777864249203656 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", "[ (11/6 → 2/1) ⇝ 13/6 | clip:1 gain:0.5 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ (11/6 → 2/1) ⇝ 13/6 | clip:1 gain:0.125 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", - "[ (11/6 → 2/1) ⇝ 5/2 | gain:0.2514806232312837 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", - "[ 3/2 ⇜ (2/1 → 13/6) | gain:0.1259345878984831 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", + "[ (11/6 → 2/1) ⇝ 5/2 | gain:0.2520845519470039 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", + "[ 3/2 ⇜ (2/1 → 13/6) | gain:0.1256534205464579 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", "[ 11/6 ⇜ (2/1 → 13/6) | clip:1 gain:0.5 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ 11/6 ⇜ (2/1 → 13/6) | clip:1 gain:0.125 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ 5/3 ⇜ (2/1 → 7/3) | gain:0.16805636796466927 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ 5/3 ⇜ (2/1 → 7/3) | gain:0.16777864249203656 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 2/1 → 7/3 | clip:1 gain:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 2/1 → 7/3 | clip:1 gain:0.25 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", - "[ 1/2 ⇜ (2/1 → 5/2) | gain:0.2523143643691359 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", - "[ 1/2 ⇜ (2/1 → 5/2) | gain:0.2523143643691359 clip:1 note:B4 s:piano release:0.1 pan:0.5787037037037037 ]", - "[ 1/2 ⇜ (2/1 → 5/2) | gain:0.2523143643691359 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ 3/2 ⇜ (2/1 → 5/2) | gain:0.2523143643691359 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", - "[ 3/2 ⇜ (2/1 → 5/2) | gain:0.12615718218456795 clip:1 note:A7 s:piano release:0.1 pan:0.7361111111111112 ]", - "[ 11/6 ⇜ (2/1 → 5/2) | gain:0.2523143643691359 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", + "[ 1/2 ⇜ (2/1 → 5/2) | gain:0.25074029392543207 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ 1/2 ⇜ (2/1 → 5/2) | gain:0.25074029392543207 clip:1 note:B4 s:piano release:0.1 pan:0.5787037037037037 ]", + "[ 1/2 ⇜ (2/1 → 5/2) | gain:0.25074029392543207 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ 3/2 ⇜ (2/1 → 5/2) | gain:0.25166796373805483 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", + "[ 3/2 ⇜ (2/1 → 5/2) | gain:0.12583398186902742 clip:1 note:A7 s:piano release:0.1 pan:0.7361111111111112 ]", + "[ 11/6 ⇜ (2/1 → 5/2) | gain:0.2520845519470039 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", "[ 2/1 → 8/3 | gain:0.5051177303460894 clip:1 note:G2 s:piano release:0.1 pan:0.44907407407407407 ]", - "[ 1/1 ⇜ (2/1 → 3/1) | gain:0.16872856115259408 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", - "[ 1/1 ⇜ (2/1 → 3/1) | gain:0.16872856115259408 clip:1 note:B5 s:piano release:0.1 pan:0.6342592592592593 ]", - "[ 1/1 ⇜ (2/1 → 3/1) | gain:0.16872856115259408 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ 3/2 ⇜ (2/1 → 3/1) ⇝ 7/2 | gain:0.12700457494822845 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ 3/2 ⇜ (2/1 → 3/1) ⇝ 7/2 | gain:0.12700457494822845 clip:1 note:B6 s:piano release:0.1 pan:0.6898148148148149 ]", - "[ 3/2 ⇜ (2/1 → 3/1) ⇝ 7/2 | gain:0.12700457494822845 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", + "[ 1/1 ⇜ (2/1 → 3/1) | gain:0.16777864249203656 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", + "[ 1/1 ⇜ (2/1 → 3/1) | gain:0.16777864249203656 clip:1 note:B5 s:piano release:0.1 pan:0.6342592592592593 ]", + "[ 1/1 ⇜ (2/1 → 3/1) | gain:0.16777864249203656 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ 3/2 ⇜ (2/1 → 3/1) ⇝ 7/2 | gain:0.12654642086444556 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", + "[ 3/2 ⇜ (2/1 → 3/1) ⇝ 7/2 | gain:0.12654642086444556 clip:1 note:B6 s:piano release:0.1 pan:0.6898148148148149 ]", + "[ 3/2 ⇜ (2/1 → 3/1) ⇝ 7/2 | gain:0.12654642086444556 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", "[ 2/1 → 3/1 | gain:0.5061856834577823 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", "[ 2/1 → 3/1 | gain:0.16872856115259408 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ (2/1 → 3/1) ⇝ 4/1 | gain:0.5101350201564457 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", @@ -6328,9 +6314,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ (5/2 → 3/1) ⇝ 19/6 | gain:0.2543459874306847 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", "[ (5/2 → 3/1) ⇝ 7/2 | gain:0.25506751007822287 clip:1 note:A5 s:piano release:0.1 pan:0.625 ]", "[ (5/2 → 3/1) ⇝ 7/2 | gain:0.12753375503911143 clip:1 note:G7 s:piano release:0.1 pan:0.7268518518518519 ]", - "[ (5/2 → 3/1) ⇝ 9/2 | gain:0.256270680283866 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", - "[ (5/2 → 3/1) ⇝ 9/2 | gain:0.256270680283866 clip:1 note:B4 s:piano release:0.1 pan:0.5787037037037037 ]", - "[ (5/2 → 3/1) ⇝ 9/2 | gain:0.256270680283866 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ (5/2 → 3/1) ⇝ 9/2 | gain:0.25762002500238423 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ (5/2 → 3/1) ⇝ 9/2 | gain:0.25762002500238423 clip:1 note:B4 s:piano release:0.1 pan:0.5787037037037037 ]", + "[ (5/2 → 3/1) ⇝ 9/2 | gain:0.25762002500238423 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", "[ 8/3 → 3/1 | clip:1 gain:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 8/3 → 3/1 | clip:1 gain:0.25 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ (8/3 → 3/1) ⇝ 10/3 | gain:0.5101350201564457 clip:1 note:G2 s:piano release:0.1 pan:0.44907407407407407 ]", @@ -6343,9 +6329,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 8/3 ⇜ (3/1 → 10/3) | gain:0.5101350201564457 clip:1 note:G2 s:piano release:0.1 pan:0.44907407407407407 ]", "[ 3/1 → 10/3 | clip:1 gain:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ 3/1 → 10/3 | clip:1 gain:0.25 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ 3/2 ⇜ (3/1 → 7/2) | gain:0.12700457494822845 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ 3/2 ⇜ (3/1 → 7/2) | gain:0.12700457494822845 clip:1 note:B6 s:piano release:0.1 pan:0.6898148148148149 ]", - "[ 3/2 ⇜ (3/1 → 7/2) | gain:0.12700457494822845 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", + "[ 3/2 ⇜ (3/1 → 7/2) | gain:0.12654642086444556 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", + "[ 3/2 ⇜ (3/1 → 7/2) | gain:0.12654642086444556 clip:1 note:B6 s:piano release:0.1 pan:0.6898148148148149 ]", + "[ 3/2 ⇜ (3/1 → 7/2) | gain:0.12654642086444556 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", "[ 5/2 ⇜ (3/1 → 7/2) | gain:0.25506751007822287 clip:1 note:A5 s:piano release:0.1 pan:0.625 ]", "[ 5/2 ⇜ (3/1 → 7/2) | gain:0.12753375503911143 clip:1 note:G7 s:piano release:0.1 pan:0.7268518518518519 ]", "[ 17/6 ⇜ (3/1 → 7/2) | gain:0.12792671070481904 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", @@ -6353,14 +6339,14 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 2/1 ⇜ (3/1 → 4/1) | gain:0.5101350201564457 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", "[ 2/1 ⇜ (3/1 → 4/1) | gain:0.5101350201564457 clip:1 note:B3 s:piano release:0.1 pan:0.5231481481481481 ]", "[ 2/1 ⇜ (3/1 → 4/1) | gain:0.5101350201564457 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ 5/2 ⇜ (3/1 → 4/1) ⇝ 9/2 | gain:0.256270680283866 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", - "[ 5/2 ⇜ (3/1 → 4/1) ⇝ 9/2 | gain:0.256270680283866 clip:1 note:B4 s:piano release:0.1 pan:0.5787037037037037 ]", - "[ 5/2 ⇜ (3/1 → 4/1) ⇝ 9/2 | gain:0.256270680283866 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ 5/2 ⇜ (3/1 → 4/1) ⇝ 9/2 | gain:0.25762002500238423 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ 5/2 ⇜ (3/1 → 4/1) ⇝ 9/2 | gain:0.25762002500238423 clip:1 note:B4 s:piano release:0.1 pan:0.5787037037037037 ]", + "[ 5/2 ⇜ (3/1 → 4/1) ⇝ 9/2 | gain:0.25762002500238423 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", "[ 3/1 → 4/1 | gain:0.5152400500047685 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 3/1 → 4/1 | gain:0.17174668333492282 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ (3/1 → 4/1) ⇝ 5/1 | gain:0.17174668333492282 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", - "[ (3/1 → 4/1) ⇝ 5/1 | gain:0.17174668333492282 clip:1 note:B5 s:piano release:0.1 pan:0.6342592592592593 ]", - "[ (3/1 → 4/1) ⇝ 5/1 | gain:0.17174668333492282 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ (3/1 → 4/1) ⇝ 5/1 | gain:0.17383743092456522 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", + "[ (3/1 → 4/1) ⇝ 5/1 | gain:0.17383743092456522 clip:1 note:B5 s:piano release:0.1 pan:0.6342592592592593 ]", + "[ (3/1 → 4/1) ⇝ 5/1 | gain:0.17383743092456522 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", "[ 19/6 → 7/2 | clip:1 gain:0.5 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ 19/6 → 7/2 | clip:1 gain:0.125 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 19/6 → 23/6 | gain:0.25762002500238423 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", @@ -6369,37 +6355,37 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 10/3 → 4/1 | gain:0.5172017373171088 clip:1 note:G2 s:piano release:0.1 pan:0.44907407407407407 ]", "[ 7/2 → 23/6 | clip:1 gain:0.5 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 7/2 → 23/6 | clip:1 gain:0.125 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", - "[ (7/2 → 4/1) ⇝ 25/6 | gain:0.1295577924390654 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", - "[ (7/2 → 4/1) ⇝ 9/2 | gain:0.2591155848781308 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", - "[ (7/2 → 4/1) ⇝ 9/2 | gain:0.1295577924390654 clip:1 note:A7 s:piano release:0.1 pan:0.7361111111111112 ]", - "[ (7/2 → 4/1) ⇝ 11/2 | gain:0.1295577924390654 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ (7/2 → 4/1) ⇝ 11/2 | gain:0.1295577924390654 clip:1 note:B6 s:piano release:0.1 pan:0.6898148148148149 ]", - "[ (7/2 → 4/1) ⇝ 11/2 | gain:0.1295577924390654 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", + "[ (7/2 → 4/1) ⇝ 25/6 | gain:0.1298232110695848 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", + "[ (7/2 → 4/1) ⇝ 9/2 | gain:0.26075614638684785 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", + "[ (7/2 → 4/1) ⇝ 9/2 | gain:0.13037807319342393 clip:1 note:A7 s:piano release:0.1 pan:0.7361111111111112 ]", + "[ (7/2 → 4/1) ⇝ 11/2 | gain:0.13223078370965538 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", + "[ (7/2 → 4/1) ⇝ 11/2 | gain:0.13223078370965538 clip:1 note:B6 s:piano release:0.1 pan:0.6898148148148149 ]", + "[ (7/2 → 4/1) ⇝ 11/2 | gain:0.13223078370965538 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", "[ 11/3 → 4/1 | clip:1 gain:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ 11/3 → 4/1 | clip:1 gain:0.25 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ (11/3 → 4/1) ⇝ 13/3 | gain:0.1730976147594464 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ (11/3 → 4/1) ⇝ 13/3 | gain:0.17383743092456522 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", "[ (23/6 → 4/1) ⇝ 25/6 | clip:1 gain:0.5 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ (23/6 → 4/1) ⇝ 25/6 | clip:1 gain:0.125 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ (23/6 → 4/1) ⇝ 9/2 | gain:0.26019330567613225 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", - "[ 7/2 ⇜ (4/1 → 25/6) | gain:0.13066742055962274 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ (23/6 → 4/1) ⇝ 9/2 | gain:0.2619292729580872 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", + "[ 7/2 ⇜ (4/1 → 25/6) | gain:0.1298232110695848 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", "[ 23/6 ⇜ (4/1 → 25/6) | clip:1 gain:0.5 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 23/6 ⇜ (4/1 → 25/6) | clip:1 gain:0.125 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ 11/3 ⇜ (4/1 → 13/3) | gain:0.17461951530539147 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 11/3 ⇜ (4/1 → 13/3) | gain:0.17383743092456522 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 4/1 → 13/3 | clip:1 gain:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", "[ 4/1 → 13/3 | clip:1 gain:0.25 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ 5/2 ⇜ (4/1 → 9/2) | gain:0.26253931151170046 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 5/2 ⇜ (4/1 → 9/2) | gain:0.26253931151170046 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", - "[ 5/2 ⇜ (4/1 → 9/2) | gain:0.26253931151170046 clip:1 note:Bb4 s:piano release:0.1 pan:0.5740740740740741 ]", - "[ 7/2 ⇜ (4/1 → 9/2) | gain:0.26253931151170046 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", - "[ 7/2 ⇜ (4/1 → 9/2) | gain:0.13126965575585023 clip:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", - "[ 23/6 ⇜ (4/1 → 9/2) | gain:0.26253931151170046 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", + "[ 5/2 ⇜ (4/1 → 9/2) | gain:0.25762002500238423 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 5/2 ⇜ (4/1 → 9/2) | gain:0.25762002500238423 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", + "[ 5/2 ⇜ (4/1 → 9/2) | gain:0.25762002500238423 clip:1 note:Bb4 s:piano release:0.1 pan:0.5740740740740741 ]", + "[ 7/2 ⇜ (4/1 → 9/2) | gain:0.26075614638684785 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ 7/2 ⇜ (4/1 → 9/2) | gain:0.13037807319342393 clip:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", + "[ 23/6 ⇜ (4/1 → 9/2) | gain:0.2619292729580872 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", "[ 4/1 → 14/3 | gain:0.5263296263974214 clip:1 note:C2 s:piano release:0.1 pan:0.41666666666666663 ]", - "[ 3/1 ⇜ (4/1 → 5/1) | gain:0.17630771161287384 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", - "[ 3/1 ⇜ (4/1 → 5/1) | gain:0.17630771161287384 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", - "[ 3/1 ⇜ (4/1 → 5/1) | gain:0.17630771161287384 clip:1 note:Bb5 s:piano release:0.1 pan:0.6296296296296297 ]", - "[ 7/2 ⇜ (4/1 → 5/1) ⇝ 11/2 | gain:0.13325917806789583 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", - "[ 7/2 ⇜ (4/1 → 5/1) ⇝ 11/2 | gain:0.13325917806789583 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", - "[ 7/2 ⇜ (4/1 → 5/1) ⇝ 11/2 | gain:0.13325917806789583 clip:1 note:Bb6 s:piano release:0.1 pan:0.6851851851851851 ]", + "[ 3/1 ⇜ (4/1 → 5/1) | gain:0.17383743092456522 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ 3/1 ⇜ (4/1 → 5/1) | gain:0.17383743092456522 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", + "[ 3/1 ⇜ (4/1 → 5/1) | gain:0.17383743092456522 clip:1 note:Bb5 s:piano release:0.1 pan:0.6296296296296297 ]", + "[ 7/2 ⇜ (4/1 → 5/1) ⇝ 11/2 | gain:0.13223078370965538 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", + "[ 7/2 ⇜ (4/1 → 5/1) ⇝ 11/2 | gain:0.13223078370965538 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", + "[ 7/2 ⇜ (4/1 → 5/1) ⇝ 11/2 | gain:0.13223078370965538 clip:1 note:Bb6 s:piano release:0.1 pan:0.6851851851851851 ]", "[ 4/1 → 5/1 | gain:0.5289231348386215 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", "[ 4/1 → 5/1 | gain:0.17630771161287384 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", "[ (4/1 → 5/1) ⇝ 6/1 | gain:0.5374082884455618 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", @@ -6416,9 +6402,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ (9/2 → 5/1) ⇝ 31/6 | gain:0.26723291891932766 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", "[ (9/2 → 5/1) ⇝ 11/2 | gain:0.2687041442227809 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", "[ (9/2 → 5/1) ⇝ 11/2 | gain:0.13435207211139044 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", - "[ (9/2 → 5/1) ⇝ 13/2 | gain:0.2710124924534749 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ (9/2 → 5/1) ⇝ 13/2 | gain:0.2710124924534749 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", - "[ (9/2 → 5/1) ⇝ 13/2 | gain:0.2710124924534749 clip:1 note:Bb4 s:piano release:0.1 pan:0.5740740740740741 ]", + "[ (9/2 → 5/1) ⇝ 13/2 | gain:0.273436125488663 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ (9/2 → 5/1) ⇝ 13/2 | gain:0.273436125488663 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", + "[ (9/2 → 5/1) ⇝ 13/2 | gain:0.273436125488663 clip:1 note:Bb4 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 14/3 → 5/1 | clip:1 gain:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", "[ 14/3 → 5/1 | clip:1 gain:0.25 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ (14/3 → 5/1) ⇝ 16/3 | gain:0.5374082884455618 clip:1 note:C2 s:piano release:0.1 pan:0.41666666666666663 ]", @@ -6431,9 +6417,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 14/3 ⇜ (5/1 → 16/3) | gain:0.5374082884455618 clip:1 note:C2 s:piano release:0.1 pan:0.41666666666666663 ]", "[ 5/1 → 16/3 | clip:1 gain:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 5/1 → 16/3 | clip:1 gain:0.25 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ 7/2 ⇜ (5/1 → 11/2) | gain:0.13325917806789583 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", - "[ 7/2 ⇜ (5/1 → 11/2) | gain:0.13325917806789583 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", - "[ 7/2 ⇜ (5/1 → 11/2) | gain:0.13325917806789583 clip:1 note:Bb6 s:piano release:0.1 pan:0.6851851851851851 ]", + "[ 7/2 ⇜ (5/1 → 11/2) | gain:0.13223078370965538 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", + "[ 7/2 ⇜ (5/1 → 11/2) | gain:0.13223078370965538 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", + "[ 7/2 ⇜ (5/1 → 11/2) | gain:0.13223078370965538 clip:1 note:Bb6 s:piano release:0.1 pan:0.6851851851851851 ]", "[ 9/2 ⇜ (5/1 → 11/2) | gain:0.2687041442227809 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", "[ 9/2 ⇜ (5/1 → 11/2) | gain:0.13435207211139044 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 29/6 ⇜ (5/1 → 11/2) | gain:0.1351149289525865 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", @@ -6441,14 +6427,14 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 4/1 ⇜ (5/1 → 6/1) | gain:0.5374082884455618 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", "[ 4/1 ⇜ (5/1 → 6/1) | gain:0.5374082884455618 clip:1 note:Eb3 s:piano release:0.1 pan:0.4861111111111111 ]", "[ 4/1 ⇜ (5/1 → 6/1) | gain:0.5374082884455618 clip:1 note:Bb3 s:piano release:0.1 pan:0.5185185185185186 ]", - "[ 9/2 ⇜ (5/1 → 6/1) ⇝ 13/2 | gain:0.2710124924534749 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 9/2 ⇜ (5/1 → 6/1) ⇝ 13/2 | gain:0.2710124924534749 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", - "[ 9/2 ⇜ (5/1 → 6/1) ⇝ 13/2 | gain:0.2710124924534749 clip:1 note:Bb4 s:piano release:0.1 pan:0.5740740740740741 ]", + "[ 9/2 ⇜ (5/1 → 6/1) ⇝ 13/2 | gain:0.273436125488663 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 9/2 ⇜ (5/1 → 6/1) ⇝ 13/2 | gain:0.273436125488663 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", + "[ 9/2 ⇜ (5/1 → 6/1) ⇝ 13/2 | gain:0.273436125488663 clip:1 note:Bb4 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 5/1 → 6/1 | gain:0.546872250977326 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 5/1 → 6/1 | gain:0.1822907503257753 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ (5/1 → 6/1) ⇝ 7/1 | gain:0.1822907503257753 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", - "[ (5/1 → 6/1) ⇝ 7/1 | gain:0.1822907503257753 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", - "[ (5/1 → 6/1) ⇝ 7/1 | gain:0.1822907503257753 clip:1 note:Bb5 s:piano release:0.1 pan:0.6296296296296297 ]", + "[ (5/1 → 6/1) ⇝ 7/1 | gain:0.18573092140656322 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ (5/1 → 6/1) ⇝ 7/1 | gain:0.18573092140656322 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", + "[ (5/1 → 6/1) ⇝ 7/1 | gain:0.18573092140656322 clip:1 note:Bb5 s:piano release:0.1 pan:0.6296296296296297 ]", "[ 31/6 → 11/2 | clip:1 gain:0.5 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 31/6 → 11/2 | clip:1 gain:0.125 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", "[ 31/6 → 35/6 | gain:0.273436125488663 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", @@ -6457,37 +6443,37 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 16/3 → 6/1 | gain:0.5502239722994923 clip:1 note:C2 s:piano release:0.1 pan:0.41666666666666663 ]", "[ 11/2 → 35/6 | clip:1 gain:0.5 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", "[ 11/2 → 35/6 | clip:1 gain:0.125 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ (11/2 → 6/1) ⇝ 37/6 | gain:0.1379835007763804 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", - "[ (11/2 → 6/1) ⇝ 13/2 | gain:0.2759670015527608 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", - "[ (11/2 → 6/1) ⇝ 13/2 | gain:0.1379835007763804 clip:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", - "[ (11/2 → 6/1) ⇝ 15/2 | gain:0.1379835007763804 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", - "[ (11/2 → 6/1) ⇝ 15/2 | gain:0.1379835007763804 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", - "[ (11/2 → 6/1) ⇝ 15/2 | gain:0.1379835007763804 clip:1 note:Bb6 s:piano release:0.1 pan:0.6851851851851851 ]", + "[ (11/2 → 6/1) ⇝ 37/6 | gain:0.1384164835303142 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ (11/2 → 6/1) ⇝ 13/2 | gain:0.27859638210984483 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ (11/2 → 6/1) ⇝ 13/2 | gain:0.13929819105492242 clip:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", + "[ (11/2 → 6/1) ⇝ 15/2 | gain:0.14205631840689176 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", + "[ (11/2 → 6/1) ⇝ 15/2 | gain:0.14205631840689176 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", + "[ (11/2 → 6/1) ⇝ 15/2 | gain:0.14205631840689176 clip:1 note:Bb6 s:piano release:0.1 pan:0.6851851851851851 ]", "[ 17/3 → 6/1 | clip:1 gain:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 17/3 → 6/1 | clip:1 gain:0.25 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ (17/3 → 6/1) ⇝ 19/3 | gain:0.18455531137375225 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ (17/3 → 6/1) ⇝ 19/3 | gain:0.18573092140656322 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (35/6 → 6/1) ⇝ 37/6 | clip:1 gain:0.5 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ (35/6 → 6/1) ⇝ 37/6 | clip:1 gain:0.125 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ (35/6 → 6/1) ⇝ 13/2 | gain:0.2777095429668417 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", - "[ 11/2 ⇜ (6/1 → 37/6) | gain:0.1397465650798828 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ (35/6 → 6/1) ⇝ 13/2 | gain:0.28039942590514827 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", + "[ 11/2 ⇜ (6/1 → 37/6) | gain:0.1384164835303142 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", "[ 35/6 ⇜ (6/1 → 37/6) | clip:1 gain:0.5 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 35/6 ⇜ (6/1 → 37/6) | clip:1 gain:0.125 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ 17/3 ⇜ (6/1 → 19/3) | gain:0.18693295060343218 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ 17/3 ⇜ (6/1 → 19/3) | gain:0.18573092140656322 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 6/1 → 19/3 | clip:1 gain:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 6/1 → 19/3 | clip:1 gain:0.25 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", - "[ 9/2 ⇜ (6/1 → 13/2) | gain:0.28131490153968597 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ 9/2 ⇜ (6/1 → 13/2) | gain:0.28131490153968597 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", - "[ 9/2 ⇜ (6/1 → 13/2) | gain:0.28131490153968597 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", - "[ 11/2 ⇜ (6/1 → 13/2) | gain:0.28131490153968597 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ 11/2 ⇜ (6/1 → 13/2) | gain:0.14065745076984298 clip:1 note:G7 s:piano release:0.1 pan:0.7268518518518519 ]", - "[ 35/6 ⇜ (6/1 → 13/2) | gain:0.28131490153968597 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", + "[ 9/2 ⇜ (6/1 → 13/2) | gain:0.273436125488663 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ 9/2 ⇜ (6/1 → 13/2) | gain:0.273436125488663 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ 9/2 ⇜ (6/1 → 13/2) | gain:0.273436125488663 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", + "[ 11/2 ⇜ (6/1 → 13/2) | gain:0.27859638210984483 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ 11/2 ⇜ (6/1 → 13/2) | gain:0.13929819105492242 clip:1 note:G7 s:piano release:0.1 pan:0.7268518518518519 ]", + "[ 35/6 ⇜ (6/1 → 13/2) | gain:0.28039942590514827 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 6/1 → 20/3 | gain:0.5644783658979073 clip:1 note:F2 s:piano release:0.1 pan:0.4398148148148148 ]", - "[ 5/1 ⇜ (6/1 → 7/1) | gain:0.18940842454252232 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ 5/1 ⇜ (6/1 → 7/1) | gain:0.18940842454252232 clip:1 note:A5 s:piano release:0.1 pan:0.625 ]", - "[ 5/1 ⇜ (6/1 → 7/1) | gain:0.18940842454252232 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", - "[ 11/2 ⇜ (6/1 → 7/1) ⇝ 15/2 | gain:0.1434895885856996 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ 11/2 ⇜ (6/1 → 7/1) ⇝ 15/2 | gain:0.1434895885856996 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ 11/2 ⇜ (6/1 → 7/1) ⇝ 15/2 | gain:0.1434895885856996 clip:1 note:Eb7 s:piano release:0.1 pan:0.7083333333333333 ]", + "[ 5/1 ⇜ (6/1 → 7/1) | gain:0.18573092140656322 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ 5/1 ⇜ (6/1 → 7/1) | gain:0.18573092140656322 clip:1 note:A5 s:piano release:0.1 pan:0.625 ]", + "[ 5/1 ⇜ (6/1 → 7/1) | gain:0.18573092140656322 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", + "[ 11/2 ⇜ (6/1 → 7/1) ⇝ 15/2 | gain:0.14205631840689176 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ 11/2 ⇜ (6/1 → 7/1) ⇝ 15/2 | gain:0.14205631840689176 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", + "[ 11/2 ⇜ (6/1 → 7/1) ⇝ 15/2 | gain:0.14205631840689176 clip:1 note:Eb7 s:piano release:0.1 pan:0.7083333333333333 ]", "[ 6/1 → 7/1 | gain:0.568225273627567 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 6/1 → 7/1 | gain:0.18940842454252232 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", "[ (6/1 → 7/1) ⇝ 8/1 | gain:0.5798073875911826 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", @@ -6504,9 +6490,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ (13/2 → 7/1) ⇝ 43/6 | gain:0.2879481196998103 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", "[ (13/2 → 7/1) ⇝ 15/2 | gain:0.2899036937955913 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", "[ (13/2 → 7/1) ⇝ 15/2 | gain:0.14495184689779564 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", - "[ (13/2 → 7/1) ⇝ 17/2 | gain:0.292875009489248 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ (13/2 → 7/1) ⇝ 17/2 | gain:0.292875009489248 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", - "[ (13/2 → 7/1) ⇝ 17/2 | gain:0.292875009489248 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", + "[ (13/2 → 7/1) ⇝ 17/2 | gain:0.29588166835112206 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ (13/2 → 7/1) ⇝ 17/2 | gain:0.29588166835112206 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ (13/2 → 7/1) ⇝ 17/2 | gain:0.29588166835112206 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", "[ 20/3 → 7/1 | clip:1 gain:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 20/3 → 7/1 | clip:1 gain:0.25 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ (20/3 → 7/1) ⇝ 22/3 | gain:0.5798073875911826 clip:1 note:F2 s:piano release:0.1 pan:0.4398148148148148 ]", @@ -6519,9 +6505,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 20/3 ⇜ (7/1 → 22/3) | gain:0.5798073875911826 clip:1 note:F2 s:piano release:0.1 pan:0.4398148148148148 ]", "[ 7/1 → 22/3 | clip:1 gain:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 7/1 → 22/3 | clip:1 gain:0.25 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ 11/2 ⇜ (7/1 → 15/2) | gain:0.1434895885856996 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ 11/2 ⇜ (7/1 → 15/2) | gain:0.1434895885856996 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ 11/2 ⇜ (7/1 → 15/2) | gain:0.1434895885856996 clip:1 note:Eb7 s:piano release:0.1 pan:0.7083333333333333 ]", + "[ 11/2 ⇜ (7/1 → 15/2) | gain:0.14205631840689176 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ 11/2 ⇜ (7/1 → 15/2) | gain:0.14205631840689176 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", + "[ 11/2 ⇜ (7/1 → 15/2) | gain:0.14205631840689176 clip:1 note:Eb7 s:piano release:0.1 pan:0.7083333333333333 ]", "[ 13/2 ⇜ (7/1 → 15/2) | gain:0.2899036937955913 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", "[ 13/2 ⇜ (7/1 → 15/2) | gain:0.14495184689779564 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", "[ 41/6 ⇜ (7/1 → 15/2) | gain:0.14594003660622698 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", @@ -6529,14 +6515,14 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 6/1 ⇜ (7/1 → 8/1) | gain:0.5798073875911826 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 6/1 ⇜ (7/1 → 8/1) | gain:0.5798073875911826 clip:1 note:A3 s:piano release:0.1 pan:0.5138888888888888 ]", "[ 6/1 ⇜ (7/1 → 8/1) | gain:0.5798073875911826 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", - "[ 13/2 ⇜ (7/1 → 8/1) ⇝ 17/2 | gain:0.292875009489248 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ 13/2 ⇜ (7/1 → 8/1) ⇝ 17/2 | gain:0.292875009489248 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", - "[ 13/2 ⇜ (7/1 → 8/1) ⇝ 17/2 | gain:0.292875009489248 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", + "[ 13/2 ⇜ (7/1 → 8/1) ⇝ 17/2 | gain:0.29588166835112206 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ 13/2 ⇜ (7/1 → 8/1) ⇝ 17/2 | gain:0.29588166835112206 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ 13/2 ⇜ (7/1 → 8/1) ⇝ 17/2 | gain:0.29588166835112206 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", "[ 7/1 → 8/1 | gain:0.5917633367022441 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 7/1 → 8/1 | gain:0.1972544455674147 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ (7/1 → 8/1) ⇝ 9/1 | gain:0.1972544455674147 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ (7/1 → 8/1) ⇝ 9/1 | gain:0.1972544455674147 clip:1 note:A5 s:piano release:0.1 pan:0.625 ]", - "[ (7/1 → 8/1) ⇝ 9/1 | gain:0.1972544455674147 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", + "[ (7/1 → 8/1) ⇝ 9/1 | gain:0.20130281100670494 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ (7/1 → 8/1) ⇝ 9/1 | gain:0.20130281100670494 clip:1 note:A5 s:piano release:0.1 pan:0.625 ]", + "[ (7/1 → 8/1) ⇝ 9/1 | gain:0.20130281100670494 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", "[ 43/6 → 15/2 | clip:1 gain:0.5 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 43/6 → 15/2 | clip:1 gain:0.125 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 43/6 → 47/6 | gain:0.29588166835112206 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", @@ -6545,37 +6531,37 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 22/3 → 8/1 | gain:0.595799977452322 clip:1 note:F2 s:piano release:0.1 pan:0.4398148148148148 ]", "[ 15/2 → 47/6 | clip:1 gain:0.5 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 15/2 → 47/6 | clip:1 gain:0.125 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", - "[ (15/2 → 8/1) ⇝ 49/6 | gain:0.14945600272593212 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ (15/2 → 8/1) ⇝ 17/2 | gain:0.29891200545186425 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ (15/2 → 8/1) ⇝ 17/2 | gain:0.14945600272593212 clip:1 note:G7 s:piano release:0.1 pan:0.7268518518518519 ]", - "[ (15/2 → 8/1) ⇝ 19/2 | gain:0.14945600272593212 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ (15/2 → 8/1) ⇝ 19/2 | gain:0.14945600272593212 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ (15/2 → 8/1) ⇝ 19/2 | gain:0.14945600272593212 clip:1 note:Eb7 s:piano release:0.1 pan:0.7083333333333333 ]", + "[ (15/2 → 8/1) ⇝ 49/6 | gain:0.1499626710398192 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ (15/2 → 8/1) ⇝ 17/2 | gain:0.30195421651005744 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ (15/2 → 8/1) ⇝ 17/2 | gain:0.15097710825502872 clip:1 note:G7 s:piano release:0.1 pan:0.7268518518518519 ]", + "[ (15/2 → 8/1) ⇝ 19/2 | gain:0.1540133823344964 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ (15/2 → 8/1) ⇝ 19/2 | gain:0.1540133823344964 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", + "[ (15/2 → 8/1) ⇝ 19/2 | gain:0.1540133823344964 clip:1 note:Eb7 s:piano release:0.1 pan:0.7083333333333333 ]", "[ 23/3 → 8/1 | clip:1 gain:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 23/3 → 8/1 | clip:1 gain:0.25 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ (23/3 → 8/1) ⇝ 25/3 | gain:0.19995022805309226 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ (23/3 → 8/1) ⇝ 25/3 | gain:0.20130281100670494 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", "[ (47/6 → 8/1) ⇝ 49/6 | clip:1 gain:0.5 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ (47/6 → 8/1) ⇝ 49/6 | clip:1 gain:0.125 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ (47/6 → 8/1) ⇝ 17/2 | gain:0.30093955912001435 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", - "[ 15/2 ⇜ (8/1 → 49/6) | gain:0.15148443695005026 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (47/6 → 8/1) ⇝ 17/2 | gain:0.3039830909404765 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", + "[ 15/2 ⇜ (8/1 → 49/6) | gain:0.1499626710398192 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", "[ 47/6 ⇜ (8/1 → 49/6) | clip:1 gain:0.5 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 47/6 ⇜ (8/1 → 49/6) | clip:1 gain:0.125 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", - "[ 23/3 ⇜ (8/1 → 25/3) | gain:0.20265539396031765 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", + "[ 23/3 ⇜ (8/1 → 25/3) | gain:0.20130281100670494 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", "[ 8/1 → 25/3 | clip:1 gain:1 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", "[ 8/1 → 25/3 | clip:1 gain:0.25 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ 13/2 ⇜ (8/1 → 17/2) | gain:0.3049964275682507 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", - "[ 13/2 ⇜ (8/1 → 17/2) | gain:0.3049964275682507 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ 13/2 ⇜ (8/1 → 17/2) | gain:0.3049964275682507 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", - "[ 15/2 ⇜ (8/1 → 17/2) | gain:0.3049964275682507 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", - "[ 15/2 ⇜ (8/1 → 17/2) | gain:0.15249821378412534 clip:1 note:E7 s:piano release:0.1 pan:0.712962962962963 ]", - "[ 47/6 ⇜ (8/1 → 17/2) | gain:0.3049964275682507 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", + "[ 13/2 ⇜ (8/1 → 17/2) | gain:0.29588166835112206 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", + "[ 13/2 ⇜ (8/1 → 17/2) | gain:0.29588166835112206 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ 13/2 ⇜ (8/1 → 17/2) | gain:0.29588166835112206 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ 15/2 ⇜ (8/1 → 17/2) | gain:0.30195421651005744 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ 15/2 ⇜ (8/1 → 17/2) | gain:0.15097710825502872 clip:1 note:E7 s:piano release:0.1 pan:0.712962962962963 ]", + "[ 47/6 ⇜ (8/1 → 17/2) | gain:0.3039830909404765 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", "[ 8/1 → 26/3 | gain:0.6120168885879078 clip:1 note:D2 s:piano release:0.1 pan:0.42592592592592593 ]", - "[ 7/1 ⇜ (8/1 → 9/1) | gain:0.2053511764459952 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", - "[ 7/1 ⇜ (8/1 → 9/1) | gain:0.2053511764459952 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ 7/1 ⇜ (8/1 → 9/1) | gain:0.2053511764459952 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", - "[ 15/2 ⇜ (8/1 → 9/1) ⇝ 19/2 | gain:0.15551671176543344 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ 15/2 ⇜ (8/1 → 9/1) ⇝ 19/2 | gain:0.15551671176543344 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ 15/2 ⇜ (8/1 → 9/1) ⇝ 19/2 | gain:0.15551671176543344 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", + "[ 7/1 ⇜ (8/1 → 9/1) | gain:0.20130281100670494 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ 7/1 ⇜ (8/1 → 9/1) | gain:0.20130281100670494 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ 7/1 ⇜ (8/1 → 9/1) | gain:0.20130281100670494 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", + "[ 15/2 ⇜ (8/1 → 9/1) ⇝ 19/2 | gain:0.1540133823344964 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", + "[ 15/2 ⇜ (8/1 → 9/1) ⇝ 19/2 | gain:0.1540133823344964 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ 15/2 ⇜ (8/1 → 9/1) ⇝ 19/2 | gain:0.1540133823344964 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 8/1 → 9/1 | gain:0.6160535293379856 clip:1 note:E4 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 8/1 → 9/1 | gain:0.2053511764459952 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", "[ (8/1 → 9/1) ⇝ 10/1 | gain:0.6280094784490473 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", @@ -6592,9 +6578,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ (17/2 → 9/1) ⇝ 55/6 | gain:0.3120283598076609 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", "[ (17/2 → 9/1) ⇝ 19/2 | gain:0.31400473922452365 clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]", "[ (17/2 → 9/1) ⇝ 19/2 | gain:0.15700236961226183 clip:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", - "[ (17/2 → 9/1) ⇝ 21/2 | gain:0.3169292558487157 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", - "[ (17/2 → 9/1) ⇝ 21/2 | gain:0.3169292558487157 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ (17/2 → 9/1) ⇝ 21/2 | gain:0.3169292558487157 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ (17/2 → 9/1) ⇝ 21/2 | gain:0.3197957962063314 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", + "[ (17/2 → 9/1) ⇝ 21/2 | gain:0.3197957962063314 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ (17/2 → 9/1) ⇝ 21/2 | gain:0.3197957962063314 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", "[ 26/3 → 9/1 | clip:1 gain:1 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", "[ 26/3 → 9/1 | clip:1 gain:0.25 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ (26/3 → 9/1) ⇝ 28/3 | gain:0.6280094784490473 clip:1 note:D2 s:piano release:0.1 pan:0.42592592592592593 ]", @@ -6607,9 +6593,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 26/3 ⇜ (9/1 → 28/3) | gain:0.6280094784490473 clip:1 note:D2 s:piano release:0.1 pan:0.42592592592592593 ]", "[ 9/1 → 28/3 | clip:1 gain:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 9/1 → 28/3 | clip:1 gain:0.25 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", - "[ 15/2 ⇜ (9/1 → 19/2) | gain:0.15551671176543344 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ 15/2 ⇜ (9/1 → 19/2) | gain:0.15551671176543344 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ 15/2 ⇜ (9/1 → 19/2) | gain:0.15551671176543344 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", + "[ 15/2 ⇜ (9/1 → 19/2) | gain:0.1540133823344964 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", + "[ 15/2 ⇜ (9/1 → 19/2) | gain:0.1540133823344964 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ 15/2 ⇜ (9/1 → 19/2) | gain:0.1540133823344964 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 17/2 ⇜ (9/1 → 19/2) | gain:0.31400473922452365 clip:1 note:E5 s:piano release:0.1 pan:0.6018518518518519 ]", "[ 17/2 ⇜ (9/1 → 19/2) | gain:0.15700236961226183 clip:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ 53/6 ⇜ (9/1 → 19/2) | gain:0.15798015666015228 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", @@ -6617,14 +6603,14 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 8/1 ⇜ (9/1 → 10/1) | gain:0.6280094784490473 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", "[ 8/1 ⇜ (9/1 → 10/1) | gain:0.6280094784490473 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 8/1 ⇜ (9/1 → 10/1) | gain:0.6280094784490473 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 17/2 ⇜ (9/1 → 10/1) ⇝ 21/2 | gain:0.3169292558487157 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", - "[ 17/2 ⇜ (9/1 → 10/1) ⇝ 21/2 | gain:0.3169292558487157 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ 17/2 ⇜ (9/1 → 10/1) ⇝ 21/2 | gain:0.3169292558487157 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ 17/2 ⇜ (9/1 → 10/1) ⇝ 21/2 | gain:0.3197957962063314 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", + "[ 17/2 ⇜ (9/1 → 10/1) ⇝ 21/2 | gain:0.3197957962063314 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ 17/2 ⇜ (9/1 → 10/1) ⇝ 21/2 | gain:0.3197957962063314 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", "[ 9/1 → 10/1 | gain:0.6395915924126628 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", "[ 9/1 → 10/1 | gain:0.2131971974708876 clip:1 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", - "[ (9/1 → 10/1) ⇝ 11/1 | gain:0.2131971974708876 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", - "[ (9/1 → 10/1) ⇝ 11/1 | gain:0.2131971974708876 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ (9/1 → 10/1) ⇝ 11/1 | gain:0.2131971974708876 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", + "[ (9/1 → 10/1) ⇝ 11/1 | gain:0.2168747006068467 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (9/1 → 10/1) ⇝ 11/1 | gain:0.2168747006068467 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ (9/1 → 10/1) ⇝ 11/1 | gain:0.2168747006068467 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", "[ 55/6 → 19/2 | clip:1 gain:0.5 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 55/6 → 19/2 | clip:1 gain:0.125 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", "[ 55/6 → 59/6 | gain:0.3197957962063314 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", @@ -6633,37 +6619,37 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 28/3 → 10/1 | gain:0.6433385001423223 clip:1 note:D2 s:piano release:0.1 pan:0.42592592592592593 ]", "[ 19/2 → 59/6 | clip:1 gain:0.5 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", "[ 19/2 → 59/6 | clip:1 gain:0.125 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ (19/2 → 10/1) ⇝ 61/6 | gain:0.16129676574021448 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", - "[ (19/2 → 10/1) ⇝ 21/2 | gain:0.32259353148042896 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", - "[ (19/2 → 10/1) ⇝ 21/2 | gain:0.16129676574021448 clip:1 note:E7 s:piano release:0.1 pan:0.712962962962963 ]", - "[ (19/2 → 10/1) ⇝ 23/2 | gain:0.16129676574021448 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ (19/2 → 10/1) ⇝ 23/2 | gain:0.16129676574021448 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ (19/2 → 10/1) ⇝ 23/2 | gain:0.16129676574021448 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", + "[ (19/2 → 10/1) ⇝ 61/6 | gain:0.16175450355748333 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (19/2 → 10/1) ⇝ 21/2 | gain:0.32531205091027005 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (19/2 → 10/1) ⇝ 21/2 | gain:0.16265602545513502 clip:1 note:E7 s:piano release:0.1 pan:0.712962962962963 ]", + "[ (19/2 → 10/1) ⇝ 23/2 | gain:0.16523615376572595 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", + "[ (19/2 → 10/1) ⇝ 23/2 | gain:0.16523615376572595 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ (19/2 → 10/1) ⇝ 23/2 | gain:0.16523615376572595 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 29/3 → 10/1 | clip:1 gain:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 29/3 → 10/1 | clip:1 gain:0.25 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", - "[ (29/3 → 10/1) ⇝ 31/3 | gain:0.21567267140997776 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", + "[ (29/3 → 10/1) ⇝ 31/3 | gain:0.2168747006068467 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", "[ (59/6 → 10/1) ⇝ 61/6 | clip:1 gain:0.5 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ (59/6 → 10/1) ⇝ 61/6 | clip:1 gain:0.125 note:E6 s:piano release:0.1 pan:0.6574074074074074 ]", - "[ (59/6 → 10/1) ⇝ 21/2 | gain:0.32441530286034925 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", - "[ 19/2 ⇜ (10/1 → 61/6) | gain:0.1630994450266366 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", + "[ (59/6 → 10/1) ⇝ 21/2 | gain:0.3270754659594865 clip:1 note:D3 s:piano release:0.1 pan:0.4814814814814815 ]", + "[ 19/2 ⇜ (10/1 → 61/6) | gain:0.16175450355748333 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", "[ 59/6 ⇜ (10/1 → 61/6) | clip:1 gain:0.5 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ 59/6 ⇜ (10/1 → 61/6) | clip:1 gain:0.125 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ 29/3 ⇜ (10/1 → 31/3) | gain:0.21805031063965766 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ 29/3 ⇜ (10/1 → 31/3) | gain:0.2168747006068467 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 10/1 → 31/3 | clip:1 gain:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 10/1 → 31/3 | clip:1 gain:0.25 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", - "[ 17/2 ⇜ (10/1 → 21/2) | gain:0.3279414314673541 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", - "[ 17/2 ⇜ (10/1 → 21/2) | gain:0.3279414314673541 clip:1 note:B4 s:piano release:0.1 pan:0.5787037037037037 ]", - "[ 17/2 ⇜ (10/1 → 21/2) | gain:0.3279414314673541 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ 19/2 ⇜ (10/1 → 21/2) | gain:0.3279414314673541 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", - "[ 19/2 ⇜ (10/1 → 21/2) | gain:0.16397071573367705 clip:1 note:A7 s:piano release:0.1 pan:0.7361111111111112 ]", - "[ 59/6 ⇜ (10/1 → 21/2) | gain:0.3279414314673541 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", + "[ 17/2 ⇜ (10/1 → 21/2) | gain:0.3197957962063314 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ 17/2 ⇜ (10/1 → 21/2) | gain:0.3197957962063314 clip:1 note:B4 s:piano release:0.1 pan:0.5787037037037037 ]", + "[ 17/2 ⇜ (10/1 → 21/2) | gain:0.3197957962063314 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ 19/2 ⇜ (10/1 → 21/2) | gain:0.32531205091027005 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", + "[ 19/2 ⇜ (10/1 → 21/2) | gain:0.16265602545513502 clip:1 note:A7 s:piano release:0.1 pan:0.7361111111111112 ]", + "[ 59/6 ⇜ (10/1 → 21/2) | gain:0.3270754659594865 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", "[ 10/1 → 32/3 | gain:0.6575928937407377 clip:1 note:G2 s:piano release:0.1 pan:0.44907407407407407 ]", - "[ 9/1 ⇜ (10/1 → 11/1) | gain:0.22031487168763458 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", - "[ 9/1 ⇜ (10/1 → 11/1) | gain:0.22031487168763458 clip:1 note:B5 s:piano release:0.1 pan:0.6342592592592593 ]", - "[ 9/1 ⇜ (10/1 → 11/1) | gain:0.22031487168763458 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ 19/2 ⇜ (10/1 → 11/1) ⇝ 23/2 | gain:0.16644797028331998 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ 19/2 ⇜ (10/1 → 11/1) ⇝ 23/2 | gain:0.16644797028331998 clip:1 note:B6 s:piano release:0.1 pan:0.6898148148148149 ]", - "[ 19/2 ⇜ (10/1 → 11/1) ⇝ 23/2 | gain:0.16644797028331998 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", + "[ 9/1 ⇜ (10/1 → 11/1) | gain:0.2168747006068467 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", + "[ 9/1 ⇜ (10/1 → 11/1) | gain:0.2168747006068467 clip:1 note:B5 s:piano release:0.1 pan:0.6342592592592593 ]", + "[ 9/1 ⇜ (10/1 → 11/1) | gain:0.2168747006068467 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ 19/2 ⇜ (10/1 → 11/1) ⇝ 23/2 | gain:0.16523615376572595 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", + "[ 19/2 ⇜ (10/1 → 11/1) ⇝ 23/2 | gain:0.16523615376572595 clip:1 note:B6 s:piano release:0.1 pan:0.6898148148148149 ]", + "[ 19/2 ⇜ (10/1 → 11/1) ⇝ 23/2 | gain:0.16523615376572595 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", "[ 10/1 → 11/1 | gain:0.6609446150629038 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", "[ 10/1 → 11/1 | gain:0.22031487168763458 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ (10/1 → 11/1) ⇝ 12/1 | gain:0.670408577594668 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", @@ -6680,9 +6666,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ (21/2 → 11/1) ⇝ 67/6 | gain:0.33367857511494187 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", "[ (21/2 → 11/1) ⇝ 23/2 | gain:0.335204288797334 clip:1 note:A5 s:piano release:0.1 pan:0.625 ]", "[ (21/2 → 11/1) ⇝ 23/2 | gain:0.167602144398667 clip:1 note:G7 s:piano release:0.1 pan:0.7268518518518519 ]", - "[ (21/2 → 11/1) ⇝ 25/2 | gain:0.33739007688432326 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", - "[ (21/2 → 11/1) ⇝ 25/2 | gain:0.33739007688432326 clip:1 note:B4 s:piano release:0.1 pan:0.5787037037037037 ]", - "[ (21/2 → 11/1) ⇝ 25/2 | gain:0.33739007688432326 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ (21/2 → 11/1) ⇝ 25/2 | gain:0.33944686560080417 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ (21/2 → 11/1) ⇝ 25/2 | gain:0.33944686560080417 clip:1 note:B4 s:piano release:0.1 pan:0.5787037037037037 ]", + "[ (21/2 → 11/1) ⇝ 25/2 | gain:0.33944686560080417 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", "[ 32/3 → 11/1 | clip:1 gain:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 32/3 → 11/1 | clip:1 gain:0.25 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ (32/3 → 11/1) ⇝ 34/3 | gain:0.670408577594668 clip:1 note:G2 s:piano release:0.1 pan:0.44907407407407407 ]", @@ -6695,9 +6681,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 32/3 ⇜ (11/1 → 34/3) | gain:0.670408577594668 clip:1 note:G2 s:piano release:0.1 pan:0.44907407407407407 ]", "[ 11/1 → 34/3 | clip:1 gain:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ 11/1 → 34/3 | clip:1 gain:0.25 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ 19/2 ⇜ (11/1 → 23/2) | gain:0.16644797028331998 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ 19/2 ⇜ (11/1 → 23/2) | gain:0.16644797028331998 clip:1 note:B6 s:piano release:0.1 pan:0.6898148148148149 ]", - "[ 19/2 ⇜ (11/1 → 23/2) | gain:0.16644797028331998 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", + "[ 19/2 ⇜ (11/1 → 23/2) | gain:0.16523615376572595 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", + "[ 19/2 ⇜ (11/1 → 23/2) | gain:0.16523615376572595 clip:1 note:B6 s:piano release:0.1 pan:0.6898148148148149 ]", + "[ 19/2 ⇜ (11/1 → 23/2) | gain:0.16523615376572595 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", "[ 21/2 ⇜ (11/1 → 23/2) | gain:0.335204288797334 clip:1 note:A5 s:piano release:0.1 pan:0.625 ]", "[ 21/2 ⇜ (11/1 → 23/2) | gain:0.167602144398667 clip:1 note:G7 s:piano release:0.1 pan:0.7268518518518519 ]", "[ 65/6 ⇜ (11/1 → 23/2) | gain:0.1683377570503936 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", @@ -6705,14 +6691,14 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 10/1 ⇜ (11/1 → 12/1) | gain:0.670408577594668 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", "[ 10/1 ⇜ (11/1 → 12/1) | gain:0.670408577594668 clip:1 note:B3 s:piano release:0.1 pan:0.5231481481481481 ]", "[ 10/1 ⇜ (11/1 → 12/1) | gain:0.670408577594668 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ 21/2 ⇜ (11/1 → 12/1) ⇝ 25/2 | gain:0.33739007688432326 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", - "[ 21/2 ⇜ (11/1 → 12/1) ⇝ 25/2 | gain:0.33739007688432326 clip:1 note:B4 s:piano release:0.1 pan:0.5787037037037037 ]", - "[ 21/2 ⇜ (11/1 → 12/1) ⇝ 25/2 | gain:0.33739007688432326 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ 21/2 ⇜ (11/1 → 12/1) ⇝ 25/2 | gain:0.33944686560080417 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ 21/2 ⇜ (11/1 → 12/1) ⇝ 25/2 | gain:0.33944686560080417 clip:1 note:B4 s:piano release:0.1 pan:0.5787037037037037 ]", + "[ 21/2 ⇜ (11/1 → 12/1) ⇝ 25/2 | gain:0.33944686560080417 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", "[ 11/1 → 12/1 | gain:0.6788937312016083 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 11/1 → 12/1 | gain:0.2262979104005361 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ (11/1 → 12/1) ⇝ 13/1 | gain:0.2262979104005361 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", - "[ (11/1 → 12/1) ⇝ 13/1 | gain:0.2262979104005361 clip:1 note:B5 s:piano release:0.1 pan:0.6342592592592593 ]", - "[ (11/1 → 12/1) ⇝ 13/1 | gain:0.2262979104005361 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ (11/1 → 12/1) ⇝ 13/1 | gain:0.22876819108884472 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", + "[ (11/1 → 12/1) ⇝ 13/1 | gain:0.22876819108884472 clip:1 note:B5 s:piano release:0.1 pan:0.6342592592592593 ]", + "[ (11/1 → 12/1) ⇝ 13/1 | gain:0.22876819108884472 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", "[ 67/6 → 23/2 | clip:1 gain:0.5 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ 67/6 → 23/2 | clip:1 gain:0.125 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 67/6 → 71/6 | gain:0.33944686560080417 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", @@ -6721,37 +6707,37 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 34/3 → 12/1 | gain:0.6814872396428084 clip:1 note:G2 s:piano release:0.1 pan:0.44907407407407407 ]", "[ 23/2 → 71/6 | clip:1 gain:0.5 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", "[ 23/2 → 71/6 | clip:1 gain:0.125 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", - "[ (23/2 → 12/1) ⇝ 73/6 | gain:0.17068456075420724 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", - "[ (23/2 → 12/1) ⇝ 25/2 | gain:0.3413691215084145 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", - "[ (23/2 → 12/1) ⇝ 25/2 | gain:0.17068456075420724 clip:1 note:A7 s:piano release:0.1 pan:0.7361111111111112 ]", - "[ (23/2 → 12/1) ⇝ 27/2 | gain:0.17068456075420724 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ (23/2 → 12/1) ⇝ 27/2 | gain:0.17068456075420724 clip:1 note:B6 s:piano release:0.1 pan:0.6898148148148149 ]", - "[ (23/2 → 12/1) ⇝ 27/2 | gain:0.17068456075420724 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", + "[ (23/2 → 12/1) ⇝ 73/6 | gain:0.17098958003101383 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", + "[ (23/2 → 12/1) ⇝ 25/2 | gain:0.3431522866332671 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", + "[ (23/2 → 12/1) ⇝ 25/2 | gain:0.17157614331663354 clip:1 note:A7 s:piano release:0.1 pan:0.7361111111111112 ]", + "[ (23/2 → 12/1) ⇝ 27/2 | gain:0.17314420400886532 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", + "[ (23/2 → 12/1) ⇝ 27/2 | gain:0.17314420400886532 clip:1 note:B6 s:piano release:0.1 pan:0.6898148148148149 ]", + "[ (23/2 → 12/1) ⇝ 27/2 | gain:0.17314420400886532 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", "[ 35/3 → 12/1 | clip:1 gain:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ 35/3 → 12/1 | clip:1 gain:0.25 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ (35/3 → 12/1) ⇝ 37/3 | gain:0.22798610670801844 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ (35/3 → 12/1) ⇝ 37/3 | gain:0.22876819108884472 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", "[ (71/6 → 12/1) ⇝ 73/6 | clip:1 gain:0.5 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", "[ (71/6 → 12/1) ⇝ 73/6 | clip:1 gain:0.125 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ (71/6 → 12/1) ⇝ 25/2 | gain:0.34257359190086933 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", - "[ 23/2 ⇜ (12/1 → 73/6) | gain:0.17185756367199134 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ (71/6 → 12/1) ⇝ 25/2 | gain:0.34426201088094527 clip:1 note:G3 s:piano release:0.1 pan:0.5046296296296297 ]", + "[ 23/2 ⇜ (12/1 → 73/6) | gain:0.17098958003101383 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", "[ 71/6 ⇜ (12/1 → 73/6) | clip:1 gain:0.5 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 71/6 ⇜ (12/1 → 73/6) | clip:1 gain:0.125 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ 35/3 ⇜ (12/1 → 37/3) | gain:0.2295080072539635 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 35/3 ⇜ (12/1 → 37/3) | gain:0.22876819108884472 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 12/1 → 37/3 | clip:1 gain:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", "[ 12/1 → 37/3 | clip:1 gain:0.25 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ 21/2 ⇜ (12/1 → 25/2) | gain:0.3447928481419841 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 21/2 ⇜ (12/1 → 25/2) | gain:0.3447928481419841 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", - "[ 21/2 ⇜ (12/1 → 25/2) | gain:0.3447928481419841 clip:1 note:Bb4 s:piano release:0.1 pan:0.5740740740740741 ]", - "[ 23/2 ⇜ (12/1 → 25/2) | gain:0.3447928481419841 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", - "[ 23/2 ⇜ (12/1 → 25/2) | gain:0.17239642407099204 clip:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", - "[ 71/6 ⇜ (12/1 → 25/2) | gain:0.3447928481419841 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", + "[ 21/2 ⇜ (12/1 → 25/2) | gain:0.33944686560080417 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 21/2 ⇜ (12/1 → 25/2) | gain:0.33944686560080417 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", + "[ 21/2 ⇜ (12/1 → 25/2) | gain:0.33944686560080417 clip:1 note:Bb4 s:piano release:0.1 pan:0.5740740740740741 ]", + "[ 23/2 ⇜ (12/1 → 25/2) | gain:0.3431522866332671 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ 23/2 ⇜ (12/1 → 25/2) | gain:0.17157614331663354 clip:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", + "[ 71/6 ⇜ (12/1 → 25/2) | gain:0.34426201088094527 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", "[ 12/1 → 38/3 | gain:0.690615128723121 clip:1 note:C2 s:piano release:0.1 pan:0.41666666666666663 ]", - "[ 11/1 ⇜ (12/1 → 13/1) | gain:0.23085893867848709 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", - "[ 11/1 ⇜ (12/1 → 13/1) | gain:0.23085893867848709 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", - "[ 11/1 ⇜ (12/1 → 13/1) | gain:0.23085893867848709 clip:1 note:Bb5 s:piano release:0.1 pan:0.6296296296296297 ]", - "[ 23/2 ⇜ (12/1 → 13/1) ⇝ 27/2 | gain:0.17381887636812446 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", - "[ 23/2 ⇜ (12/1 → 13/1) ⇝ 27/2 | gain:0.17381887636812446 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", - "[ 23/2 ⇜ (12/1 → 13/1) ⇝ 27/2 | gain:0.17381887636812446 clip:1 note:Bb6 s:piano release:0.1 pan:0.6851851851851851 ]", + "[ 11/1 ⇜ (12/1 → 13/1) | gain:0.22876819108884472 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ 11/1 ⇜ (12/1 → 13/1) | gain:0.22876819108884472 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", + "[ 11/1 ⇜ (12/1 → 13/1) | gain:0.22876819108884472 clip:1 note:Bb5 s:piano release:0.1 pan:0.6296296296296297 ]", + "[ 23/2 ⇜ (12/1 → 13/1) ⇝ 27/2 | gain:0.17314420400886532 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", + "[ 23/2 ⇜ (12/1 → 13/1) ⇝ 27/2 | gain:0.17314420400886532 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", + "[ 23/2 ⇜ (12/1 → 13/1) ⇝ 27/2 | gain:0.17314420400886532 clip:1 note:Bb6 s:piano release:0.1 pan:0.6851851851851851 ]", "[ 12/1 → 13/1 | gain:0.6925768160354613 clip:1 note:D4 s:piano release:0.1 pan:0.537037037037037 ]", "[ 12/1 → 13/1 | gain:0.23085893867848709 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", "[ (12/1 → 13/1) ⇝ 14/1 | gain:0.697681845883784 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", @@ -6768,9 +6754,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ (25/2 → 13/1) ⇝ 79/6 | gain:0.34805501161047686 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", "[ (25/2 → 13/1) ⇝ 27/2 | gain:0.348840922941892 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", "[ (25/2 → 13/1) ⇝ 27/2 | gain:0.174420461470946 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", - "[ (25/2 → 13/1) ⇝ 29/2 | gain:0.34989928312365803 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ (25/2 → 13/1) ⇝ 29/2 | gain:0.34989928312365803 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", - "[ (25/2 → 13/1) ⇝ 29/2 | gain:0.34989928312365803 clip:1 note:Bb4 s:piano release:0.1 pan:0.5740740740740741 ]", + "[ (25/2 → 13/1) ⇝ 29/2 | gain:0.35081559129122375 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ (25/2 → 13/1) ⇝ 29/2 | gain:0.35081559129122375 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", + "[ (25/2 → 13/1) ⇝ 29/2 | gain:0.35081559129122375 clip:1 note:Bb4 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 38/3 → 13/1 | clip:1 gain:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", "[ 38/3 → 13/1 | clip:1 gain:0.25 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ (38/3 → 13/1) ⇝ 40/3 | gain:0.697681845883784 clip:1 note:C2 s:piano release:0.1 pan:0.41666666666666663 ]", @@ -6783,9 +6769,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 38/3 ⇜ (13/1 → 40/3) | gain:0.697681845883784 clip:1 note:C2 s:piano release:0.1 pan:0.41666666666666663 ]", "[ 13/1 → 40/3 | clip:1 gain:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 13/1 → 40/3 | clip:1 gain:0.25 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ 23/2 ⇜ (13/1 → 27/2) | gain:0.17381887636812446 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", - "[ 23/2 ⇜ (13/1 → 27/2) | gain:0.17381887636812446 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", - "[ 23/2 ⇜ (13/1 → 27/2) | gain:0.17381887636812446 clip:1 note:Bb6 s:piano release:0.1 pan:0.6851851851851851 ]", + "[ 23/2 ⇜ (13/1 → 27/2) | gain:0.17314420400886532 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", + "[ 23/2 ⇜ (13/1 → 27/2) | gain:0.17314420400886532 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", + "[ 23/2 ⇜ (13/1 → 27/2) | gain:0.17314420400886532 clip:1 note:Bb6 s:piano release:0.1 pan:0.6851851851851851 ]", "[ 25/2 ⇜ (13/1 → 27/2) | gain:0.348840922941892 clip:1 note:D5 s:piano release:0.1 pan:0.5925925925925926 ]", "[ 25/2 ⇜ (13/1 → 27/2) | gain:0.174420461470946 clip:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 77/6 ⇜ (13/1 → 27/2) | gain:0.1747812227947151 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", @@ -6793,14 +6779,14 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 12/1 ⇜ (13/1 → 14/1) | gain:0.697681845883784 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", "[ 12/1 ⇜ (13/1 → 14/1) | gain:0.697681845883784 clip:1 note:Eb3 s:piano release:0.1 pan:0.4861111111111111 ]", "[ 12/1 ⇜ (13/1 → 14/1) | gain:0.697681845883784 clip:1 note:Bb3 s:piano release:0.1 pan:0.5185185185185186 ]", - "[ 25/2 ⇜ (13/1 → 14/1) ⇝ 29/2 | gain:0.34989928312365803 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 25/2 ⇜ (13/1 → 14/1) ⇝ 29/2 | gain:0.34989928312365803 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", - "[ 25/2 ⇜ (13/1 → 14/1) ⇝ 29/2 | gain:0.34989928312365803 clip:1 note:Bb4 s:piano release:0.1 pan:0.5740740740740741 ]", + "[ 25/2 ⇜ (13/1 → 14/1) ⇝ 29/2 | gain:0.35081559129122375 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 25/2 ⇜ (13/1 → 14/1) ⇝ 29/2 | gain:0.35081559129122375 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", + "[ 25/2 ⇜ (13/1 → 14/1) ⇝ 29/2 | gain:0.35081559129122375 clip:1 note:Bb4 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 13/1 → 14/1 | gain:0.7016311825824475 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 13/1 → 14/1 | gain:0.23387706086081583 clip:1 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ (13/1 → 14/1) ⇝ 15/1 | gain:0.23387706086081583 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", - "[ (13/1 → 14/1) ⇝ 15/1 | gain:0.23387706086081583 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", - "[ (13/1 → 14/1) ⇝ 15/1 | gain:0.23387706086081583 clip:1 note:Bb5 s:piano release:0.1 pan:0.6296296296296297 ]", + "[ (13/1 → 14/1) ⇝ 15/1 | gain:0.23482697952137338 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ (13/1 → 14/1) ⇝ 15/1 | gain:0.23482697952137338 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", + "[ (13/1 → 14/1) ⇝ 15/1 | gain:0.23482697952137338 clip:1 note:Bb5 s:piano release:0.1 pan:0.6296296296296297 ]", "[ 79/6 → 27/2 | clip:1 gain:0.5 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 79/6 → 27/2 | clip:1 gain:0.125 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", "[ 79/6 → 83/6 | gain:0.35081559129122375 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", @@ -6809,37 +6795,37 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 40/3 → 14/1 | gain:0.7026991356941406 clip:1 note:C2 s:piano release:0.1 pan:0.41666666666666663 ]", "[ 27/2 → 83/6 | clip:1 gain:0.5 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", "[ 27/2 → 83/6 | clip:1 gain:0.125 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ (27/2 → 14/1) ⇝ 85/6 | gain:0.1757970343254895 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", - "[ (27/2 → 14/1) ⇝ 29/2 | gain:0.351594068650979 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", - "[ (27/2 → 14/1) ⇝ 29/2 | gain:0.1757970343254895 clip:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", - "[ (27/2 → 14/1) ⇝ 31/2 | gain:0.1757970343254895 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", - "[ (27/2 → 14/1) ⇝ 31/2 | gain:0.1757970343254895 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", - "[ (27/2 → 14/1) ⇝ 31/2 | gain:0.1757970343254895 clip:1 note:Bb6 s:piano release:0.1 pan:0.6851851851851851 ]", + "[ (27/2 → 14/1) ⇝ 85/6 | gain:0.17591194053655546 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ (27/2 → 14/1) ⇝ 29/2 | gain:0.3522404692820601 clip:1 note:C5 s:piano release:0.1 pan:0.5833333333333333 ]", + "[ (27/2 → 14/1) ⇝ 29/2 | gain:0.17612023464103005 clip:1 note:D7 s:piano release:0.1 pan:0.7037037037037037 ]", + "[ (27/2 → 14/1) ⇝ 31/2 | gain:0.17658406954734143 clip:1 note:C6 s:piano release:0.1 pan:0.6388888888888888 ]", + "[ (27/2 → 14/1) ⇝ 31/2 | gain:0.17658406954734143 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", + "[ (27/2 → 14/1) ⇝ 31/2 | gain:0.17658406954734143 clip:1 note:Bb6 s:piano release:0.1 pan:0.6851851851851851 ]", "[ 41/3 → 14/1 | clip:1 gain:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 41/3 → 14/1 | clip:1 gain:0.25 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ (41/3 → 14/1) ⇝ 43/3 | gain:0.2345492540487406 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ (41/3 → 14/1) ⇝ 43/3 | gain:0.23482697952137338 clip:1 note:C4 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (83/6 → 14/1) ⇝ 85/6 | clip:1 gain:0.5 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ (83/6 → 14/1) ⇝ 85/6 | clip:1 gain:0.125 note:D6 s:piano release:0.1 pan:0.6481481481481481 ]", - "[ (83/6 → 14/1) ⇝ 29/2 | gain:0.3520392572231487 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", - "[ 27/2 ⇜ (14/1 → 85/6) | gain:0.17621390489441566 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ (83/6 → 14/1) ⇝ 29/2 | gain:0.3526015919271991 clip:1 note:C3 s:piano release:0.1 pan:0.4722222222222222 ]", + "[ 27/2 ⇜ (14/1 → 85/6) | gain:0.17591194053655546 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", "[ 83/6 ⇜ (14/1 → 85/6) | clip:1 gain:0.5 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 83/6 ⇜ (14/1 → 85/6) | clip:1 gain:0.125 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ 41/3 ⇜ (14/1 → 43/3) | gain:0.23506772795146605 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ 41/3 ⇜ (14/1 → 43/3) | gain:0.23482697952137338 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 14/1 → 43/3 | clip:1 gain:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 14/1 → 43/3 | clip:1 gain:0.25 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", - "[ 25/2 ⇜ (14/1 → 29/2) | gain:0.35276214981238413 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ 25/2 ⇜ (14/1 → 29/2) | gain:0.35276214981238413 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", - "[ 25/2 ⇜ (14/1 → 29/2) | gain:0.35276214981238413 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", - "[ 27/2 ⇜ (14/1 → 29/2) | gain:0.35276214981238413 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ 27/2 ⇜ (14/1 → 29/2) | gain:0.17638107490619206 clip:1 note:G7 s:piano release:0.1 pan:0.7268518518518519 ]", - "[ 83/6 ⇜ (14/1 → 29/2) | gain:0.35276214981238413 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", + "[ 25/2 ⇜ (14/1 → 29/2) | gain:0.35081559129122375 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ 25/2 ⇜ (14/1 → 29/2) | gain:0.35081559129122375 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ 25/2 ⇜ (14/1 → 29/2) | gain:0.35081559129122375 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", + "[ 27/2 ⇜ (14/1 → 29/2) | gain:0.3522404692820601 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ 27/2 ⇜ (14/1 → 29/2) | gain:0.17612023464103005 clip:1 note:G7 s:piano release:0.1 pan:0.7268518518518519 ]", + "[ 83/6 ⇜ (14/1 → 29/2) | gain:0.3526015919271991 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 14/1 → 44/3 | gain:0.7058196775556453 clip:1 note:F2 s:piano release:0.1 pan:0.4398148148148148 ]", - "[ 13/1 ⇜ (14/1 → 15/1) | gain:0.2354454260631219 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ 13/1 ⇜ (14/1 → 15/1) | gain:0.2354454260631219 clip:1 note:A5 s:piano release:0.1 pan:0.625 ]", - "[ 13/1 ⇜ (14/1 → 15/1) | gain:0.2354454260631219 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", - "[ 27/2 ⇜ (14/1 → 15/1) ⇝ 31/2 | gain:0.17673460394126828 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ 27/2 ⇜ (14/1 → 15/1) ⇝ 31/2 | gain:0.17673460394126828 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ 27/2 ⇜ (14/1 → 15/1) ⇝ 31/2 | gain:0.17673460394126828 clip:1 note:Eb7 s:piano release:0.1 pan:0.7083333333333333 ]", + "[ 13/1 ⇜ (14/1 → 15/1) | gain:0.23482697952137338 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ 13/1 ⇜ (14/1 → 15/1) | gain:0.23482697952137338 clip:1 note:A5 s:piano release:0.1 pan:0.625 ]", + "[ 13/1 ⇜ (14/1 → 15/1) | gain:0.23482697952137338 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", + "[ 27/2 ⇜ (14/1 → 15/1) ⇝ 31/2 | gain:0.17658406954734143 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ 27/2 ⇜ (14/1 → 15/1) ⇝ 31/2 | gain:0.17658406954734143 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", + "[ 27/2 ⇜ (14/1 → 15/1) ⇝ 31/2 | gain:0.17658406954734143 clip:1 note:Eb7 s:piano release:0.1 pan:0.7083333333333333 ]", "[ 14/1 → 15/1 | gain:0.7063362781893657 clip:1 note:G4 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 14/1 → 15/1 | gain:0.2354454260631219 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", "[ (14/1 → 15/1) ⇝ 16/1 | gain:0.7073558770128159 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", @@ -6856,9 +6842,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ (29/2 → 15/1) ⇝ 91/6 | gain:0.3535483696800781 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", "[ (29/2 → 15/1) ⇝ 31/2 | gain:0.35367793850640794 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", "[ (29/2 → 15/1) ⇝ 31/2 | gain:0.17683896925320397 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", - "[ (29/2 → 15/1) ⇝ 33/2 | gain:0.3538087945496763 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ (29/2 → 15/1) ⇝ 33/2 | gain:0.3538087945496763 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", - "[ (29/2 → 15/1) ⇝ 33/2 | gain:0.3538087945496763 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", + "[ (29/2 → 15/1) ⇝ 33/2 | gain:0.35387819052467123 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ (29/2 → 15/1) ⇝ 33/2 | gain:0.35387819052467123 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ (29/2 → 15/1) ⇝ 33/2 | gain:0.35387819052467123 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", "[ 44/3 → 15/1 | clip:1 gain:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 44/3 → 15/1 | clip:1 gain:0.25 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ (44/3 → 15/1) ⇝ 46/3 | gain:0.7073558770128159 clip:1 note:F2 s:piano release:0.1 pan:0.4398148148148148 ]", @@ -6871,9 +6857,9 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 44/3 ⇜ (15/1 → 46/3) | gain:0.7073558770128159 clip:1 note:F2 s:piano release:0.1 pan:0.4398148148148148 ]", "[ 15/1 → 46/3 | clip:1 gain:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 15/1 → 46/3 | clip:1 gain:0.25 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ 27/2 ⇜ (15/1 → 31/2) | gain:0.17673460394126828 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ 27/2 ⇜ (15/1 → 31/2) | gain:0.17673460394126828 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ 27/2 ⇜ (15/1 → 31/2) | gain:0.17673460394126828 clip:1 note:Eb7 s:piano release:0.1 pan:0.7083333333333333 ]", + "[ 27/2 ⇜ (15/1 → 31/2) | gain:0.17658406954734143 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ 27/2 ⇜ (15/1 → 31/2) | gain:0.17658406954734143 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", + "[ 27/2 ⇜ (15/1 → 31/2) | gain:0.17658406954734143 clip:1 note:Eb7 s:piano release:0.1 pan:0.7083333333333333 ]", "[ 29/2 ⇜ (15/1 → 31/2) | gain:0.35367793850640794 clip:1 note:G5 s:piano release:0.1 pan:0.6157407407407407 ]", "[ 29/2 ⇜ (15/1 → 31/2) | gain:0.17683896925320397 clip:1 note:F7 s:piano release:0.1 pan:0.7175925925925926 ]", "[ 89/6 ⇜ (15/1 → 31/2) | gain:0.17688642813272723 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", @@ -6881,14 +6867,14 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 14/1 ⇜ (15/1 → 16/1) | gain:0.7073558770128159 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 14/1 ⇜ (15/1 → 16/1) | gain:0.7073558770128159 clip:1 note:A3 s:piano release:0.1 pan:0.5138888888888888 ]", "[ 14/1 ⇜ (15/1 → 16/1) | gain:0.7073558770128159 clip:1 note:Eb4 s:piano release:0.1 pan:0.5416666666666667 ]", - "[ 29/2 ⇜ (15/1 → 16/1) ⇝ 33/2 | gain:0.3538087945496763 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ 29/2 ⇜ (15/1 → 16/1) ⇝ 33/2 | gain:0.3538087945496763 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", - "[ 29/2 ⇜ (15/1 → 16/1) ⇝ 33/2 | gain:0.3538087945496763 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", + "[ 29/2 ⇜ (15/1 → 16/1) ⇝ 33/2 | gain:0.35387819052467123 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ 29/2 ⇜ (15/1 → 16/1) ⇝ 33/2 | gain:0.35387819052467123 clip:1 note:A4 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ 29/2 ⇜ (15/1 → 16/1) ⇝ 33/2 | gain:0.35387819052467123 clip:1 note:Eb5 s:piano release:0.1 pan:0.5972222222222222 ]", "[ 15/1 → 16/1 | gain:0.7077563810493425 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 15/1 → 16/1 | gain:0.23591879368311414 clip:1 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ (15/1 → 16/1) ⇝ 17/1 | gain:0.23591879368311414 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ (15/1 → 16/1) ⇝ 17/1 | gain:0.23591879368311414 clip:1 note:A5 s:piano release:0.1 pan:0.625 ]", - "[ (15/1 → 16/1) ⇝ 17/1 | gain:0.23591879368311414 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", + "[ (15/1 → 16/1) ⇝ 17/1 | gain:0.23593895534674325 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ (15/1 → 16/1) ⇝ 17/1 | gain:0.23593895534674325 clip:1 note:A5 s:piano release:0.1 pan:0.625 ]", + "[ (15/1 → 16/1) ⇝ 17/1 | gain:0.23593895534674325 clip:1 note:Eb6 s:piano release:0.1 pan:0.6527777777777778 ]", "[ 91/6 → 31/2 | clip:1 gain:0.5 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 91/6 → 31/2 | clip:1 gain:0.125 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 91/6 → 95/6 | gain:0.35387819052467123 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", @@ -6897,18 +6883,18 @@ exports[`renders tunes > tune: festivalOfFingers3 1`] = ` "[ 46/3 → 16/1 | gain:0.7077986570641782 clip:1 note:F2 s:piano release:0.1 pan:0.4398148148148148 ]", "[ 31/2 → 95/6 | clip:1 gain:0.5 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", "[ 31/2 → 95/6 | clip:1 gain:0.125 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", - "[ (31/2 → 16/1) ⇝ 97/6 | gain:0.17695228077435335 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ (31/2 → 16/1) ⇝ 33/2 | gain:0.3539045615487067 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", - "[ (31/2 → 16/1) ⇝ 33/2 | gain:0.17695228077435335 clip:1 note:G7 s:piano release:0.1 pan:0.7268518518518519 ]", - "[ (31/2 → 16/1) ⇝ 35/2 | gain:0.17695228077435335 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", - "[ (31/2 → 16/1) ⇝ 35/2 | gain:0.17695228077435335 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", - "[ (31/2 → 16/1) ⇝ 35/2 | gain:0.17695228077435335 clip:1 note:Eb7 s:piano release:0.1 pan:0.7083333333333333 ]", + "[ (31/2 → 16/1) ⇝ 97/6 | gain:0.17695363841880415 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ (31/2 → 16/1) ⇝ 33/2 | gain:0.3539084330201149 clip:1 note:F5 s:piano release:0.1 pan:0.6064814814814814 ]", + "[ (31/2 → 16/1) ⇝ 33/2 | gain:0.17695421651005744 clip:1 note:G7 s:piano release:0.1 pan:0.7268518518518519 ]", + "[ (31/2 → 16/1) ⇝ 35/2 | gain:0.17696702224930969 clip:1 note:F6 s:piano release:0.1 pan:0.662037037037037 ]", + "[ (31/2 → 16/1) ⇝ 35/2 | gain:0.17696702224930969 clip:1 note:A6 s:piano release:0.1 pan:0.6805555555555556 ]", + "[ (31/2 → 16/1) ⇝ 35/2 | gain:0.17696702224930969 clip:1 note:Eb7 s:piano release:0.1 pan:0.7083333333333333 ]", "[ 47/3 → 16/1 | clip:1 gain:1 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ 47/3 → 16/1 | clip:1 gain:0.25 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ (47/3 → 16/1) ⇝ 49/3 | gain:0.23593818455840554 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", + "[ (47/3 → 16/1) ⇝ 49/3 | gain:0.23593895534674325 clip:1 note:F4 s:piano release:0.1 pan:0.5509259259259259 ]", "[ (95/6 → 16/1) ⇝ 97/6 | clip:1 gain:0.5 note:C7 s:piano release:0.1 pan:0.6944444444444444 ]", "[ (95/6 → 16/1) ⇝ 97/6 | clip:1 gain:0.125 note:G6 s:piano release:0.1 pan:0.6712962962962963 ]", - "[ (95/6 → 16/1) ⇝ 33/2 | gain:0.3539082873575392 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", + "[ (95/6 → 16/1) ⇝ 33/2 | gain:0.3539094121570209 clip:1 note:F3 s:piano release:0.1 pan:0.49537037037037035 ]", ] `; @@ -6943,55 +6929,34 @@ exports[`renders tunes > tune: giantSteps 1`] = ` "[ 0/1 → 5/8 | note:Db5 ]", "[ 0/1 → 5/8 | note:Eb5 ]", "[ 0/1 → 5/8 | note:B2 ]", - "[ (5/8 → 1/1) ⇝ 5/4 | note:D5 ]", - "[ (5/8 → 1/1) ⇝ 5/4 | note:C4 ]", - "[ (5/8 → 1/1) ⇝ 5/4 | note:E4 ]", - "[ (5/8 → 1/1) ⇝ 5/4 | note:Gb4 ]", - "[ (5/8 → 1/1) ⇝ 5/4 | note:B4 ]", - "[ (5/8 → 1/1) ⇝ 5/4 | note:D2 ]", - "[ 5/8 ⇜ (1/1 → 5/4) | note:D5 ]", - "[ 5/8 ⇜ (1/1 → 5/4) | note:C4 ]", - "[ 5/8 ⇜ (1/1 → 5/4) | note:E4 ]", - "[ 5/8 ⇜ (1/1 → 5/4) | note:Gb4 ]", - "[ 5/8 ⇜ (1/1 → 5/4) | note:B4 ]", - "[ 5/8 ⇜ (1/1 → 5/4) | note:D2 ]", + "[ 5/8 → 5/4 | note:D5 ]", + "[ 5/8 → 5/4 | note:C4 ]", + "[ 5/8 → 5/4 | note:E4 ]", + "[ 5/8 → 5/4 | note:Gb4 ]", + "[ 5/8 → 5/4 | note:B4 ]", + "[ 5/8 → 5/4 | note:D2 ]", "[ 5/4 → 15/8 | note:B4 ]", "[ 5/4 → 15/8 | note:B3 ]", "[ 5/4 → 15/8 | note:D4 ]", "[ 5/4 → 15/8 | note:Gb4 ]", "[ 5/4 → 15/8 | note:A4 ]", "[ 5/4 → 15/8 | note:G2 ]", - "[ (15/8 → 2/1) ⇝ 5/2 | note:G4 ]", - "[ (15/8 → 2/1) ⇝ 5/2 | note:Ab3 ]", - "[ (15/8 → 2/1) ⇝ 5/2 | note:C4 ]", - "[ (15/8 → 2/1) ⇝ 5/2 | note:D4 ]", - "[ (15/8 → 2/1) ⇝ 5/2 | note:Bb2 ]", - "[ 15/8 ⇜ (2/1 → 5/2) | note:G4 ]", - "[ 15/8 ⇜ (2/1 → 5/2) | note:Ab3 ]", - "[ 15/8 ⇜ (2/1 → 5/2) | note:C4 ]", - "[ 15/8 ⇜ (2/1 → 5/2) | note:D4 ]", - "[ 15/8 ⇜ (2/1 → 5/2) | note:Bb2 ]", - "[ (5/2 → 3/1) ⇝ 25/8 | note:Eb2 ]", - "[ (5/2 → 3/1) ⇝ 15/4 | note:Bb4 ]", - "[ (5/2 → 3/1) ⇝ 15/4 | note:D4 ]", - "[ (5/2 → 3/1) ⇝ 15/4 | note:F4 ]", - "[ (5/2 → 3/1) ⇝ 15/4 | note:G4 ]", - "[ 5/2 ⇜ (3/1 → 25/8) | note:Eb2 ]", - "[ 5/2 ⇜ (3/1 → 15/4) | note:Bb4 ]", - "[ 5/2 ⇜ (3/1 → 15/4) | note:D4 ]", - "[ 5/2 ⇜ (3/1 → 15/4) | note:F4 ]", - "[ 5/2 ⇜ (3/1 → 15/4) | note:G4 ]", + "[ 15/8 → 5/2 | note:G4 ]", + "[ 15/8 → 5/2 | note:Ab3 ]", + "[ 15/8 → 5/2 | note:C4 ]", + "[ 15/8 → 5/2 | note:D4 ]", + "[ 15/8 → 5/2 | note:Bb2 ]", + "[ 5/2 → 25/8 | note:Eb2 ]", + "[ 5/2 → 15/4 | note:Bb4 ]", + "[ 5/2 → 15/4 | note:D4 ]", + "[ 5/2 → 15/4 | note:F4 ]", + "[ 5/2 → 15/4 | note:G4 ]", "[ 25/8 → 15/4 | note:Bb3 ]", - "[ (15/4 → 4/1) ⇝ 35/8 | note:B4 ]", - "[ (15/4 → 4/1) ⇝ 35/8 | note:C4 ]", - "[ (15/4 → 4/1) ⇝ 35/8 | note:E4 ]", - "[ (15/4 → 4/1) ⇝ 35/8 | note:G4 ]", - "[ (15/4 → 4/1) ⇝ 35/8 | note:A2 ]", - "[ 15/4 ⇜ (4/1 → 35/8) | note:B4 ]", - "[ 15/4 ⇜ (4/1 → 35/8) | note:C4 ]", - "[ 15/4 ⇜ (4/1 → 35/8) | note:E4 ]", - "[ 15/4 ⇜ (4/1 → 35/8) | note:G4 ]", - "[ 15/4 ⇜ (4/1 → 35/8) | note:A2 ]", + "[ 15/4 → 35/8 | note:B4 ]", + "[ 15/4 → 35/8 | note:C4 ]", + "[ 15/4 → 35/8 | note:E4 ]", + "[ 15/4 → 35/8 | note:G4 ]", + "[ 15/4 → 35/8 | note:A2 ]", "[ 35/8 → 5/1 | note:A4 ]", "[ 35/8 → 5/1 | note:Gb3 ]", "[ 35/8 → 5/1 | note:B3 ]", @@ -7003,55 +6968,34 @@ exports[`renders tunes > tune: giantSteps 1`] = ` "[ 5/1 → 45/8 | note:A4 ]", "[ 5/1 → 45/8 | note:B4 ]", "[ 5/1 → 45/8 | note:G2 ]", - "[ (45/8 → 6/1) ⇝ 25/4 | note:Bb4 ]", - "[ (45/8 → 6/1) ⇝ 25/4 | note:Ab3 ]", - "[ (45/8 → 6/1) ⇝ 25/4 | note:C4 ]", - "[ (45/8 → 6/1) ⇝ 25/4 | note:D4 ]", - "[ (45/8 → 6/1) ⇝ 25/4 | note:G4 ]", - "[ (45/8 → 6/1) ⇝ 25/4 | note:Bb2 ]", - "[ 45/8 ⇜ (6/1 → 25/4) | note:Bb4 ]", - "[ 45/8 ⇜ (6/1 → 25/4) | note:Ab3 ]", - "[ 45/8 ⇜ (6/1 → 25/4) | note:C4 ]", - "[ 45/8 ⇜ (6/1 → 25/4) | note:D4 ]", - "[ 45/8 ⇜ (6/1 → 25/4) | note:G4 ]", - "[ 45/8 ⇜ (6/1 → 25/4) | note:Bb2 ]", + "[ 45/8 → 25/4 | note:Bb4 ]", + "[ 45/8 → 25/4 | note:Ab3 ]", + "[ 45/8 → 25/4 | note:C4 ]", + "[ 45/8 → 25/4 | note:D4 ]", + "[ 45/8 → 25/4 | note:G4 ]", + "[ 45/8 → 25/4 | note:Bb2 ]", "[ 25/4 → 55/8 | note:G4 ]", "[ 25/4 → 55/8 | note:G3 ]", "[ 25/4 → 55/8 | note:Bb3 ]", "[ 25/4 → 55/8 | note:D4 ]", "[ 25/4 → 55/8 | note:F4 ]", "[ 25/4 → 55/8 | note:Eb2 ]", - "[ (55/8 → 7/1) ⇝ 15/2 | note:Eb4 ]", - "[ (55/8 → 7/1) ⇝ 15/2 | note:E3 ]", - "[ (55/8 → 7/1) ⇝ 15/2 | note:Ab3 ]", - "[ (55/8 → 7/1) ⇝ 15/2 | note:Bb3 ]", - "[ (55/8 → 7/1) ⇝ 15/2 | note:F#2 ]", - "[ 55/8 ⇜ (7/1 → 15/2) | note:Eb4 ]", - "[ 55/8 ⇜ (7/1 → 15/2) | note:E3 ]", - "[ 55/8 ⇜ (7/1 → 15/2) | note:Ab3 ]", - "[ 55/8 ⇜ (7/1 → 15/2) | note:Bb3 ]", - "[ 55/8 ⇜ (7/1 → 15/2) | note:F#2 ]", - "[ (15/2 → 8/1) ⇝ 65/8 | note:B2 ]", - "[ (15/2 → 8/1) ⇝ 35/4 | note:F#4 ]", - "[ (15/2 → 8/1) ⇝ 35/4 | note:Bb3 ]", - "[ (15/2 → 8/1) ⇝ 35/4 | note:Db4 ]", - "[ (15/2 → 8/1) ⇝ 35/4 | note:Eb4 ]", - "[ 15/2 ⇜ (8/1 → 65/8) | note:B2 ]", - "[ 15/2 ⇜ (8/1 → 35/4) | note:F#4 ]", - "[ 15/2 ⇜ (8/1 → 35/4) | note:Bb3 ]", - "[ 15/2 ⇜ (8/1 → 35/4) | note:Db4 ]", - "[ 15/2 ⇜ (8/1 → 35/4) | note:Eb4 ]", + "[ 55/8 → 15/2 | note:Eb4 ]", + "[ 55/8 → 15/2 | note:E3 ]", + "[ 55/8 → 15/2 | note:Ab3 ]", + "[ 55/8 → 15/2 | note:Bb3 ]", + "[ 55/8 → 15/2 | note:F#2 ]", + "[ 15/2 → 65/8 | note:B2 ]", + "[ 15/2 → 35/4 | note:F#4 ]", + "[ 15/2 → 35/4 | note:Bb3 ]", + "[ 15/2 → 35/4 | note:Db4 ]", + "[ 15/2 → 35/4 | note:Eb4 ]", "[ 65/8 → 35/4 | note:F#2 ]", - "[ (35/4 → 9/1) ⇝ 75/8 | note:G4 ]", - "[ (35/4 → 9/1) ⇝ 75/8 | note:Ab3 ]", - "[ (35/4 → 9/1) ⇝ 75/8 | note:C4 ]", - "[ (35/4 → 9/1) ⇝ 75/8 | note:Eb4 ]", - "[ (35/4 → 9/1) ⇝ 75/8 | note:F2 ]", - "[ 35/4 ⇜ (9/1 → 75/8) | note:G4 ]", - "[ 35/4 ⇜ (9/1 → 75/8) | note:Ab3 ]", - "[ 35/4 ⇜ (9/1 → 75/8) | note:C4 ]", - "[ 35/4 ⇜ (9/1 → 75/8) | note:Eb4 ]", - "[ 35/4 ⇜ (9/1 → 75/8) | note:F2 ]", + "[ 35/4 → 75/8 | note:G4 ]", + "[ 35/4 → 75/8 | note:Ab3 ]", + "[ 35/4 → 75/8 | note:C4 ]", + "[ 35/4 → 75/8 | note:Eb4 ]", + "[ 35/4 → 75/8 | note:F2 ]", "[ 75/8 → 10/1 | note:F4 ]", "[ 75/8 → 10/1 | note:D3 ]", "[ 75/8 → 10/1 | note:G3 ]", @@ -7059,54 +7003,33 @@ exports[`renders tunes > tune: giantSteps 1`] = ` "[ 75/8 → 10/1 | note:C4 ]", "[ 75/8 → 10/1 | note:Bb2 ]", "[ 10/1 → 85/8 | note:Eb2 ]", - "[ (10/1 → 11/1) ⇝ 45/4 | note:Bb4 ]", - "[ (10/1 → 11/1) ⇝ 45/4 | note:D4 ]", - "[ (10/1 → 11/1) ⇝ 45/4 | note:F4 ]", - "[ (10/1 → 11/1) ⇝ 45/4 | note:G4 ]", - "[ (85/8 → 11/1) ⇝ 45/4 | note:Bb2 ]", - "[ 10/1 ⇜ (11/1 → 45/4) | note:Bb4 ]", - "[ 10/1 ⇜ (11/1 → 45/4) | note:D4 ]", - "[ 10/1 ⇜ (11/1 → 45/4) | note:F4 ]", - "[ 10/1 ⇜ (11/1 → 45/4) | note:G4 ]", - "[ 85/8 ⇜ (11/1 → 45/4) | note:Bb2 ]", + "[ 10/1 → 45/4 | note:Bb4 ]", + "[ 10/1 → 45/4 | note:D4 ]", + "[ 10/1 → 45/4 | note:F4 ]", + "[ 10/1 → 45/4 | note:G4 ]", + "[ 85/8 → 45/4 | note:Bb2 ]", "[ 45/4 → 95/8 | note:B4 ]", "[ 45/4 → 95/8 | note:C4 ]", "[ 45/4 → 95/8 | note:E4 ]", "[ 45/4 → 95/8 | note:G4 ]", "[ 45/4 → 95/8 | note:A2 ]", - "[ (95/8 → 12/1) ⇝ 25/2 | note:A4 ]", - "[ (95/8 → 12/1) ⇝ 25/2 | note:Gb3 ]", - "[ (95/8 → 12/1) ⇝ 25/2 | note:B3 ]", - "[ (95/8 → 12/1) ⇝ 25/2 | note:C4 ]", - "[ (95/8 → 12/1) ⇝ 25/2 | note:E4 ]", - "[ (95/8 → 12/1) ⇝ 25/2 | note:D2 ]", - "[ 95/8 ⇜ (12/1 → 25/2) | note:A4 ]", - "[ 95/8 ⇜ (12/1 → 25/2) | note:Gb3 ]", - "[ 95/8 ⇜ (12/1 → 25/2) | note:B3 ]", - "[ 95/8 ⇜ (12/1 → 25/2) | note:C4 ]", - "[ 95/8 ⇜ (12/1 → 25/2) | note:E4 ]", - "[ 95/8 ⇜ (12/1 → 25/2) | note:D2 ]", - "[ (25/2 → 13/1) ⇝ 105/8 | note:G2 ]", - "[ (25/2 → 13/1) ⇝ 55/4 | note:D5 ]", - "[ (25/2 → 13/1) ⇝ 55/4 | note:Gb4 ]", - "[ (25/2 → 13/1) ⇝ 55/4 | note:A4 ]", - "[ (25/2 → 13/1) ⇝ 55/4 | note:B4 ]", - "[ 25/2 ⇜ (13/1 → 105/8) | note:G2 ]", - "[ 25/2 ⇜ (13/1 → 55/4) | note:D5 ]", - "[ 25/2 ⇜ (13/1 → 55/4) | note:Gb4 ]", - "[ 25/2 ⇜ (13/1 → 55/4) | note:A4 ]", - "[ 25/2 ⇜ (13/1 → 55/4) | note:B4 ]", + "[ 95/8 → 25/2 | note:A4 ]", + "[ 95/8 → 25/2 | note:Gb3 ]", + "[ 95/8 → 25/2 | note:B3 ]", + "[ 95/8 → 25/2 | note:C4 ]", + "[ 95/8 → 25/2 | note:E4 ]", + "[ 95/8 → 25/2 | note:D2 ]", + "[ 25/2 → 105/8 | note:G2 ]", + "[ 25/2 → 55/4 | note:D5 ]", + "[ 25/2 → 55/4 | note:Gb4 ]", + "[ 25/2 → 55/4 | note:A4 ]", + "[ 25/2 → 55/4 | note:B4 ]", "[ 105/8 → 55/4 | note:D2 ]", - "[ (55/4 → 14/1) ⇝ 115/8 | note:D#5 ]", - "[ (55/4 → 14/1) ⇝ 115/8 | note:E4 ]", - "[ (55/4 → 14/1) ⇝ 115/8 | note:Ab4 ]", - "[ (55/4 → 14/1) ⇝ 115/8 | note:B4 ]", - "[ (55/4 → 14/1) ⇝ 115/8 | note:C#2 ]", - "[ 55/4 ⇜ (14/1 → 115/8) | note:D#5 ]", - "[ 55/4 ⇜ (14/1 → 115/8) | note:E4 ]", - "[ 55/4 ⇜ (14/1 → 115/8) | note:Ab4 ]", - "[ 55/4 ⇜ (14/1 → 115/8) | note:B4 ]", - "[ 55/4 ⇜ (14/1 → 115/8) | note:C#2 ]", + "[ 55/4 → 115/8 | note:D#5 ]", + "[ 55/4 → 115/8 | note:E4 ]", + "[ 55/4 → 115/8 | note:Ab4 ]", + "[ 55/4 → 115/8 | note:B4 ]", + "[ 55/4 → 115/8 | note:C#2 ]", "[ 115/8 → 15/1 | note:C#5 ]", "[ 115/8 → 15/1 | note:Bb3 ]", "[ 115/8 → 15/1 | note:Eb4 ]", @@ -7114,56 +7037,34 @@ exports[`renders tunes > tune: giantSteps 1`] = ` "[ 115/8 → 15/1 | note:Ab4 ]", "[ 115/8 → 15/1 | note:F#2 ]", "[ 15/1 → 125/8 | note:B2 ]", - "[ (15/1 → 16/1) ⇝ 65/4 | note:F#5 ]", - "[ (15/1 → 16/1) ⇝ 65/4 | note:Bb4 ]", - "[ (15/1 → 16/1) ⇝ 65/4 | note:Db5 ]", - "[ (15/1 → 16/1) ⇝ 65/4 | note:Eb5 ]", - "[ (125/8 → 16/1) ⇝ 65/4 | note:F#2 ]", - "[ 15/1 ⇜ (16/1 → 65/4) | note:F#5 ]", - "[ 15/1 ⇜ (16/1 → 65/4) | note:Bb4 ]", - "[ 15/1 ⇜ (16/1 → 65/4) | note:Db5 ]", - "[ 15/1 ⇜ (16/1 → 65/4) | note:Eb5 ]", - "[ 125/8 ⇜ (16/1 → 65/4) | note:F#2 ]", + "[ 15/1 → 65/4 | note:F#5 ]", + "[ 15/1 → 65/4 | note:Bb4 ]", + "[ 15/1 → 65/4 | note:Db5 ]", + "[ 15/1 → 65/4 | note:Eb5 ]", + "[ 125/8 → 65/4 | note:F#2 ]", "[ 65/4 → 135/8 | note:G5 ]", "[ 65/4 → 135/8 | note:Ab4 ]", "[ 65/4 → 135/8 | note:C5 ]", "[ 65/4 → 135/8 | note:Eb5 ]", "[ 65/4 → 135/8 | note:F2 ]", - "[ (135/8 → 17/1) ⇝ 35/2 | note:F5 ]", - "[ (135/8 → 17/1) ⇝ 35/2 | note:D4 ]", - "[ (135/8 → 17/1) ⇝ 35/2 | note:G4 ]", - "[ (135/8 → 17/1) ⇝ 35/2 | note:Ab4 ]", - "[ (135/8 → 17/1) ⇝ 35/2 | note:C5 ]", - "[ (135/8 → 17/1) ⇝ 35/2 | note:Bb2 ]", - "[ 135/8 ⇜ (17/1 → 35/2) | note:F5 ]", - "[ 135/8 ⇜ (17/1 → 35/2) | note:D4 ]", - "[ 135/8 ⇜ (17/1 → 35/2) | note:G4 ]", - "[ 135/8 ⇜ (17/1 → 35/2) | note:Ab4 ]", - "[ 135/8 ⇜ (17/1 → 35/2) | note:C5 ]", - "[ 135/8 ⇜ (17/1 → 35/2) | note:Bb2 ]", - "[ (35/2 → 18/1) ⇝ 145/8 | note:Eb2 ]", - "[ (35/2 → 18/1) ⇝ 75/4 | note:Bb5 ]", - "[ (35/2 → 18/1) ⇝ 75/4 | note:D5 ]", - "[ (35/2 → 18/1) ⇝ 75/4 | note:F5 ]", - "[ (35/2 → 18/1) ⇝ 75/4 | note:G5 ]", - "[ 35/2 ⇜ (18/1 → 145/8) | note:Eb2 ]", - "[ 35/2 ⇜ (18/1 → 75/4) | note:Bb5 ]", - "[ 35/2 ⇜ (18/1 → 75/4) | note:D5 ]", - "[ 35/2 ⇜ (18/1 → 75/4) | note:F5 ]", - "[ 35/2 ⇜ (18/1 → 75/4) | note:G5 ]", + "[ 135/8 → 35/2 | note:F5 ]", + "[ 135/8 → 35/2 | note:D4 ]", + "[ 135/8 → 35/2 | note:G4 ]", + "[ 135/8 → 35/2 | note:Ab4 ]", + "[ 135/8 → 35/2 | note:C5 ]", + "[ 135/8 → 35/2 | note:Bb2 ]", + "[ 35/2 → 145/8 | note:Eb2 ]", + "[ 35/2 → 75/4 | note:Bb5 ]", + "[ 35/2 → 75/4 | note:D5 ]", + "[ 35/2 → 75/4 | note:F5 ]", + "[ 35/2 → 75/4 | note:G5 ]", "[ 145/8 → 75/4 | note:Bb3 ]", - "[ (75/4 → 19/1) ⇝ 155/8 | note:F#5 ]", - "[ (75/4 → 19/1) ⇝ 155/8 | note:E4 ]", - "[ (75/4 → 19/1) ⇝ 155/8 | note:Ab4 ]", - "[ (75/4 → 19/1) ⇝ 155/8 | note:B4 ]", - "[ (75/4 → 19/1) ⇝ 155/8 | note:Eb5 ]", - "[ (75/4 → 19/1) ⇝ 155/8 | note:C#2 ]", - "[ 75/4 ⇜ (19/1 → 155/8) | note:F#5 ]", - "[ 75/4 ⇜ (19/1 → 155/8) | note:E4 ]", - "[ 75/4 ⇜ (19/1 → 155/8) | note:Ab4 ]", - "[ 75/4 ⇜ (19/1 → 155/8) | note:B4 ]", - "[ 75/4 ⇜ (19/1 → 155/8) | note:Eb5 ]", - "[ 75/4 ⇜ (19/1 → 155/8) | note:C#2 ]", + "[ 75/4 → 155/8 | note:F#5 ]", + "[ 75/4 → 155/8 | note:E4 ]", + "[ 75/4 → 155/8 | note:Ab4 ]", + "[ 75/4 → 155/8 | note:B4 ]", + "[ 75/4 → 155/8 | note:Eb5 ]", + "[ 75/4 → 155/8 | note:C#2 ]", "[ 155/8 → 20/1 | note:F#5 ]", "[ 155/8 → 20/1 | note:E4 ]", "[ 155/8 → 20/1 | note:Ab4 ]", @@ -7309,29 +7210,29 @@ exports[`renders tunes > tune: holyflute 1`] = ` exports[`renders tunes > tune: hyperpop 1`] = ` [ - "[ -1/4 ⇜ (0/1 → 1/12) ⇝ 1/8 | gain:0.0002512336761852884 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]", - "[ -1/4 ⇜ (0/1 → 1/12) ⇝ 1/8 | gain:0.0002512336761852884 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]", - "[ -3/8 ⇜ (0/1 → 1/8) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:1666.5665766857219 ]", - "[ -3/8 ⇜ (0/1 → 1/8) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:1666.5665766857219 ]", - "[ -1/8 ⇜ (0/1 → 1/6) ⇝ 1/4 | gain:0.0003185964356240245 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.143312438893 cutoff:4000 ]", - "[ -1/8 ⇜ (0/1 → 1/6) ⇝ 1/4 | gain:0.0003185964356240245 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.143312438893 cutoff:4000 ]", - "[ -1/4 ⇜ (0/1 → 1/4) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:1683.1306585059317 ]", - "[ -1/4 ⇜ (0/1 → 1/4) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:1683.1306585059317 ]", + "[ -1/4 ⇜ (0/1 → 1/12) ⇝ 1/8 | gain:0.00024394233952886464 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]", + "[ -1/4 ⇜ (0/1 → 1/12) ⇝ 1/8 | gain:0.00024394233952886464 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]", + "[ -3/8 ⇜ (0/1 → 1/8) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:1616.8693414940683 ]", + "[ -3/8 ⇜ (0/1 → 1/8) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:1616.8693414940683 ]", + "[ -1/8 ⇜ (0/1 → 1/6) ⇝ 1/4 | gain:0.00031404209523161047 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]", + "[ -1/8 ⇜ (0/1 → 1/6) ⇝ 1/4 | gain:0.00031404209523161047 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]", + "[ -1/4 ⇜ (0/1 → 1/4) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:1650 ]", + "[ -1/4 ⇜ (0/1 → 1/4) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:1650 ]", "[ 0/1 → 1/4 | gain:0.00039824554453003064 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.143312438893 cutoff:4000 ]", "[ 0/1 → 1/4 | gain:0.00039824554453003064 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.143312438893 cutoff:4000 ]", "[ 0/1 → 1/4 | s:bd gain:0.7 ]", "[ (0/1 → 1/3) ⇝ 3/8 | gain:0.26103468453995016 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5998.072590601808 cutoff:4000 ]", "[ (0/1 → 1/3) ⇝ 3/8 | gain:0.26103468453995016 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5998.072590601808 cutoff:4000 ]", - "[ -1/8 ⇜ (0/1 → 3/8) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:1699.6897509708342 ]", - "[ -1/8 ⇜ (0/1 → 3/8) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:1699.6897509708342 ]", - "[ -1/4 ⇜ (1/12 → 1/8) | gain:0.0002512336761852884 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]", - "[ -1/4 ⇜ (1/12 → 1/8) | gain:0.0002512336761852884 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]", + "[ -1/8 ⇜ (0/1 → 3/8) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:1683.1306585059317 ]", + "[ -1/8 ⇜ (0/1 → 3/8) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:1683.1306585059317 ]", + "[ -1/4 ⇜ (1/12 → 1/8) | gain:0.00024394233952886464 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]", + "[ -1/4 ⇜ (1/12 → 1/8) | gain:0.00024394233952886464 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]", "[ 1/8 → 1/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1699.6897509708342 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 1/8 → 1/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1699.6897509708342 lpattack:0.1 lpenv:2 ftype:24db ]", "[ (1/8 → 5/12) ⇝ 1/2 | gain:0.0002657724569848846 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5994.647308096509 cutoff:4000 ]", "[ (1/8 → 5/12) ⇝ 1/2 | gain:0.0002657724569848846 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5994.647308096509 cutoff:4000 ]", - "[ -1/8 ⇜ (1/6 → 1/4) | gain:0.0003185964356240245 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.143312438893 cutoff:4000 ]", - "[ -1/8 ⇜ (1/6 → 1/4) | gain:0.0003185964356240245 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.143312438893 cutoff:4000 ]", + "[ -1/8 ⇜ (1/6 → 1/4) | gain:0.00031404209523161047 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]", + "[ -1/8 ⇜ (1/6 → 1/4) | gain:0.00031404209523161047 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]", "[ 1/4 → 1/2 | gain:0.0003367315392180906 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5992.29333433282 cutoff:4000 ]", "[ 1/4 → 1/2 | gain:0.0003367315392180906 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5992.29333433282 cutoff:4000 ]", "[ 1/4 → 1/2 | s:hh3 gain:0.7 ]", @@ -7364,20 +7265,16 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 3/4 → 1/1 | gain:0.300533478008833 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5958.137268909887 cutoff:4000 ]", "[ 3/4 → 1/1 | gain:0.300533478008833 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5958.137268909887 cutoff:4000 ]", "[ 3/4 → 1/1 | s:hh3 gain:0.7 ]", - "[ (3/4 → 1/1) ⇝ 9/8 | gain:0.15563993880588714 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5951.963201008076 cutoff:4000 ]", - "[ (3/4 → 1/1) ⇝ 9/8 | gain:0.15563993880588714 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5951.963201008076 cutoff:4000 ]", + "[ (3/4 → 13/12) ⇝ 9/8 | gain:0.15563993880588714 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5951.963201008076 cutoff:4000 ]", + "[ (3/4 → 13/12) ⇝ 9/8 | gain:0.15563993880588714 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5951.963201008076 cutoff:4000 ]", "[ 1/2 ⇜ (5/6 → 7/8) | gain:0.18560442471759028 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5974.128467049176 cutoff:4000 ]", "[ 1/2 ⇜ (5/6 → 7/8) | gain:0.18560442471759028 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5974.128467049176 cutoff:4000 ]", "[ 7/8 → 1/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1897.1038487394403 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 7/8 → 1/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1897.1038487394403 lpattack:0.1 lpenv:2 ftype:24db ]", - "[ (7/8 → 1/1) ⇝ 5/4 | gain:0.1989031661444791 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5938.355801271282 cutoff:4000 ]", - "[ (7/8 → 1/1) ⇝ 5/4 | gain:0.1989031661444791 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5938.355801271282 cutoff:4000 ]", + "[ (7/8 → 7/6) ⇝ 5/4 | gain:0.1989031661444791 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5938.355801271282 cutoff:4000 ]", + "[ (7/8 → 7/6) ⇝ 5/4 | gain:0.1989031661444791 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5938.355801271282 cutoff:4000 ]", "[ 5/8 ⇜ (11/12 → 1/1) | gain:0.237641808847867 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5963.890147645195 cutoff:4000 ]", "[ 5/8 ⇜ (11/12 → 1/1) | gain:0.237641808847867 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5963.890147645195 cutoff:4000 ]", - "[ 3/4 ⇜ (1/1 → 13/12) ⇝ 9/8 | gain:0.15563993880588714 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5951.963201008076 cutoff:4000 ]", - "[ 3/4 ⇜ (1/1 → 13/12) ⇝ 9/8 | gain:0.15563993880588714 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5951.963201008076 cutoff:4000 ]", - "[ 7/8 ⇜ (1/1 → 7/6) ⇝ 5/4 | gain:0.1989031661444791 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5938.355801271282 cutoff:4000 ]", - "[ 7/8 ⇜ (1/1 → 7/6) ⇝ 5/4 | gain:0.1989031661444791 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5938.355801271282 cutoff:4000 ]", "[ 1/1 → 5/4 | gain:0.2513066112116339 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5930.924800994192 cutoff:4000 ]", "[ 1/1 → 5/4 | gain:0.2513066112116339 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5930.924800994192 cutoff:4000 ]", "[ 1/1 → 5/4 | s:bd gain:0.7 ]", @@ -7418,8 +7315,8 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 5/4 ⇜ (19/12 → 13/8) | gain:0.10821620301269062 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5887.549861142967 cutoff:4000 ]", "[ (13/8 → 23/12) ⇝ 2/1 | gain:0.11402475157686406 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5822.02388217981 cutoff:4000 ]", "[ (13/8 → 23/12) ⇝ 2/1 | gain:0.11402475157686406 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5822.02388217981 cutoff:4000 ]", - "[ (13/8 → 2/1) ⇝ 17/8 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2120.3652183367367 ]", - "[ (13/8 → 2/1) ⇝ 17/8 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2120.3652183367367 ]", + "[ (13/8 → 2/1) ⇝ 17/8 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2135.8582993222344 ]", + "[ (13/8 → 2/1) ⇝ 17/8 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2135.8582993222344 ]", "[ 11/8 ⇜ (5/3 → 7/4) | gain:0.13777765528071248 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5867.325323737765 cutoff:4000 ]", "[ 11/8 ⇜ (5/3 → 7/4) | gain:0.13777765528071248 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5867.325323737765 cutoff:4000 ]", "[ 7/4 → 15/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2120.3652183367367 lpattack:0.1 lpenv:2 ftype:24db ]", @@ -7427,43 +7324,43 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 7/4 → 2/1 | gain:0.14366058218580086 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5809.698831278217 cutoff:4000 ]", "[ 7/4 → 2/1 | gain:0.14366058218580086 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5809.698831278217 cutoff:4000 ]", "[ 7/4 → 2/1 | s:hh3 gain:0.7 ]", - "[ (7/4 → 2/1) ⇝ 17/8 | gain:0.07355421807913005 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5809.698831278217 cutoff:4000 ]", - "[ (7/4 → 2/1) ⇝ 17/8 | gain:0.07355421807913005 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5809.698831278217 cutoff:4000 ]", - "[ (7/4 → 2/1) ⇝ 9/4 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2135.8582993222344 ]", - "[ (7/4 → 2/1) ⇝ 9/4 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2135.8582993222344 ]", + "[ (7/4 → 2/1) ⇝ 17/8 | gain:0.07411986998714647 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5796.978025372246 cutoff:4000 ]", + "[ (7/4 → 2/1) ⇝ 17/8 | gain:0.07411986998714647 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5796.978025372246 cutoff:4000 ]", + "[ (7/4 → 2/1) ⇝ 9/4 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2166.622633692871 ]", + "[ (7/4 → 2/1) ⇝ 9/4 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2166.622633692871 ]", "[ 3/2 ⇜ (11/6 → 15/8) | gain:0.08972789051217522 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5845.47833980621 cutoff:4000 ]", "[ 3/2 ⇜ (11/6 → 15/8) | gain:0.08972789051217522 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5845.47833980621 cutoff:4000 ]", "[ 15/8 → 2/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2151.2782118349805 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 15/8 → 2/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2151.2782118349805 lpattack:0.1 lpenv:2 ftype:24db ]", - "[ (15/8 → 2/1) ⇝ 9/4 | gain:0.09264983748393309 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5796.978025372246 cutoff:4000 ]", - "[ (15/8 → 2/1) ⇝ 9/4 | gain:0.09264983748393309 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5796.978025372246 cutoff:4000 ]", - "[ (15/8 → 2/1) ⇝ 19/8 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2151.2782118349805 ]", - "[ (15/8 → 2/1) ⇝ 19/8 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2151.2782118349805 ]", + "[ (15/8 → 2/1) ⇝ 9/4 | gain:0.09401455409698445 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5770.357934562703 cutoff:4000 ]", + "[ (15/8 → 2/1) ⇝ 9/4 | gain:0.09401455409698445 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5770.357934562703 cutoff:4000 ]", + "[ (15/8 → 2/1) ⇝ 19/8 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2197.0757739067362 ]", + "[ (15/8 → 2/1) ⇝ 19/8 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2197.0757739067362 ]", "[ 13/8 ⇜ (23/12 → 2/1) | gain:0.11402475157686406 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5822.02388217981 cutoff:4000 ]", "[ 13/8 ⇜ (23/12 → 2/1) | gain:0.11402475157686406 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5822.02388217981 cutoff:4000 ]", - "[ 7/4 ⇜ (2/1 → 25/12) ⇝ 17/8 | gain:0.07521164327758756 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5770.357934562703 cutoff:4000 ]", - "[ 7/4 ⇜ (2/1 → 25/12) ⇝ 17/8 | gain:0.07521164327758756 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5770.357934562703 cutoff:4000 ]", - "[ 13/8 ⇜ (2/1 → 17/8) | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2181.889254082415 ]", - "[ 13/8 ⇜ (2/1 → 17/8) | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2181.889254082415 ]", - "[ 15/8 ⇜ (2/1 → 13/6) ⇝ 9/4 | gain:0.09467138377075762 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5756.463210874651 cutoff:4000 ]", - "[ 15/8 ⇜ (2/1 → 13/6) ⇝ 9/4 | gain:0.09467138377075762 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5756.463210874651 cutoff:4000 ]", - "[ 7/4 ⇜ (2/1 → 9/4) | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2197.0757739067362 ]", - "[ 7/4 ⇜ (2/1 → 9/4) | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2197.0757739067362 ]", + "[ 7/4 ⇜ (2/1 → 25/12) ⇝ 17/8 | gain:0.07411986998714647 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5796.978025372246 cutoff:4000 ]", + "[ 7/4 ⇜ (2/1 → 25/12) ⇝ 17/8 | gain:0.07411986998714647 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5796.978025372246 cutoff:4000 ]", + "[ 13/8 ⇜ (2/1 → 17/8) | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2135.8582993222344 ]", + "[ 13/8 ⇜ (2/1 → 17/8) | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2135.8582993222344 ]", + "[ 15/8 ⇜ (2/1 → 13/6) ⇝ 9/4 | gain:0.09401455409698445 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5770.357934562703 cutoff:4000 ]", + "[ 15/8 ⇜ (2/1 → 13/6) ⇝ 9/4 | gain:0.09401455409698445 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5770.357934562703 cutoff:4000 ]", + "[ 7/4 ⇜ (2/1 → 9/4) | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2166.622633692871 ]", + "[ 7/4 ⇜ (2/1 → 9/4) | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2166.622633692871 ]", "[ 2/1 → 9/4 | gain:0.11833922971344701 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5756.463210874651 cutoff:4000 ]", "[ 2/1 → 9/4 | gain:0.11833922971344701 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5756.463210874651 cutoff:4000 ]", "[ 2/1 → 9/4 | s:bd gain:0.7 ]", "[ (2/1 → 7/3) ⇝ 19/8 | gain:0.06099882456242525 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5742.18185383172 cutoff:4000 ]", "[ (2/1 → 7/3) ⇝ 19/8 | gain:0.06099882456242525 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5742.18185383172 cutoff:4000 ]", - "[ 15/8 ⇜ (2/1 → 19/8) | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2212.17990613181 ]", - "[ 15/8 ⇜ (2/1 → 19/8) | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2212.17990613181 ]", - "[ 7/4 ⇜ (25/12 → 17/8) | gain:0.07521164327758756 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5770.357934562703 cutoff:4000 ]", - "[ 7/4 ⇜ (25/12 → 17/8) | gain:0.07521164327758756 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5770.357934562703 cutoff:4000 ]", + "[ 15/8 ⇜ (2/1 → 19/8) | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2197.0757739067362 ]", + "[ 15/8 ⇜ (2/1 → 19/8) | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2197.0757739067362 ]", + "[ 7/4 ⇜ (25/12 → 17/8) | gain:0.07411986998714647 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5796.978025372246 cutoff:4000 ]", + "[ 7/4 ⇜ (25/12 → 17/8) | gain:0.07411986998714647 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5796.978025372246 cutoff:4000 ]", "[ 17/8 → 9/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2212.17990613181 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 17/8 → 9/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2212.17990613181 lpattack:0.1 lpenv:2 ftype:24db ]", "[ (17/8 → 29/12) ⇝ 5/2 | gain:0.07722803431084992 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5712.469093657604 cutoff:4000 ]", "[ (17/8 → 29/12) ⇝ 5/2 | gain:0.07722803431084992 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5712.469093657604 cutoff:4000 ]", - "[ 15/8 ⇜ (13/6 → 9/4) | gain:0.09467138377075762 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5756.463210874651 cutoff:4000 ]", - "[ 15/8 ⇜ (13/6 → 9/4) | gain:0.09467138377075762 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5756.463210874651 cutoff:4000 ]", + "[ 15/8 ⇜ (13/6 → 9/4) | gain:0.09401455409698445 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5770.357934562703 cutoff:4000 ]", + "[ 15/8 ⇜ (13/6 → 9/4) | gain:0.09401455409698445 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5770.357934562703 cutoff:4000 ]", "[ 9/4 → 5/2 | gain:0.09711940526986938 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5697.042781654914 cutoff:4000 ]", "[ 9/4 → 5/2 | gain:0.09711940526986938 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5697.042781654914 cutoff:4000 ]", "[ 9/4 → 5/2 | s:hh3 gain:0.7 ]", @@ -7496,20 +7393,16 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 11/4 → 3/1 | gain:0.06469267544862903 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5560.31547155504 cutoff:4000 ]", "[ 11/4 → 3/1 | gain:0.06469267544862903 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5560.31547155504 cutoff:4000 ]", "[ 11/4 → 3/1 | s:hh3 gain:0.7 ]", - "[ (11/4 → 3/1) ⇝ 25/8 | gain:0.033254339487292464 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5541.603887904197 cutoff:4000 ]", - "[ (11/4 → 3/1) ⇝ 25/8 | gain:0.033254339487292464 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5541.603887904197 cutoff:4000 ]", + "[ (11/4 → 37/12) ⇝ 25/8 | gain:0.033254339487292464 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5541.603887904197 cutoff:4000 ]", + "[ (11/4 → 37/12) ⇝ 25/8 | gain:0.033254339487292464 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5541.603887904197 cutoff:4000 ]", "[ 5/2 ⇜ (17/6 → 23/8) | gain:0.04085727749307612 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5614.319554259933 cutoff:4000 ]", "[ 5/2 ⇜ (17/6 → 23/8) | gain:0.04085727749307612 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5614.319554259933 cutoff:4000 ]", "[ 23/8 → 3/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2386.1887343697626 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 23/8 → 3/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2386.1887343697626 lpattack:0.1 lpenv:2 ftype:24db ]", - "[ (23/8 → 3/1) ⇝ 13/4 | gain:0.041870446443995187 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5503.134531727652 cutoff:4000 ]", - "[ (23/8 → 3/1) ⇝ 13/4 | gain:0.041870446443995187 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5503.134531727652 cutoff:4000 ]", + "[ (23/8 → 19/6) ⇝ 13/4 | gain:0.041870446443995187 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5503.134531727652 cutoff:4000 ]", + "[ (23/8 → 19/6) ⇝ 13/4 | gain:0.041870446443995187 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5503.134531727652 cutoff:4000 ]", "[ 21/8 ⇜ (35/12 → 3/1) | gain:0.051537412445127495 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5578.674030756363 cutoff:4000 ]", "[ 21/8 ⇜ (35/12 → 3/1) | gain:0.051537412445127495 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5578.674030756363 cutoff:4000 ]", - "[ 11/4 ⇜ (3/1 → 37/12) ⇝ 25/8 | gain:0.033254339487292464 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5541.603887904197 cutoff:4000 ]", - "[ 11/4 ⇜ (3/1 → 37/12) ⇝ 25/8 | gain:0.033254339487292464 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5541.603887904197 cutoff:4000 ]", - "[ 23/8 ⇜ (3/1 → 19/6) ⇝ 13/4 | gain:0.041870446443995187 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5503.134531727652 cutoff:4000 ]", - "[ 23/8 ⇜ (3/1 → 19/6) ⇝ 13/4 | gain:0.041870446443995187 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5503.134531727652 cutoff:4000 ]", "[ 3/1 → 13/4 | gain:0.05251021778611238 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5483.383350728088 cutoff:4000 ]", "[ 3/1 → 13/4 | gain:0.05251021778611238 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5483.383350728088 cutoff:4000 ]", "[ 3/1 → 13/4 | s:bd gain:0.7 ]", @@ -7550,8 +7443,8 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 13/4 ⇜ (43/12 → 29/8) | gain:0.021789864126373813 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5379.599518697443 cutoff:4000 ]", "[ (29/8 → 47/12) ⇝ 4/1 | gain:0.02196788874761195 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5244.4761496042 cutoff:4000 ]", "[ (29/8 → 47/12) ⇝ 4/1 | gain:0.02196788874761195 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5244.4761496042 cutoff:4000 ]", - "[ (29/8 → 4/1) ⇝ 33/8 | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2568.811347023862 ]", - "[ (29/8 → 4/1) ⇝ 33/8 | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2568.811347023862 ]", + "[ (29/8 → 4/1) ⇝ 33/8 | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2580.8797353950404 ]", + "[ (29/8 → 4/1) ⇝ 33/8 | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2580.8797353950404 ]", "[ 27/8 ⇜ (11/3 → 15/4) | gain:0.02733603378769718 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5335.806273589214 cutoff:4000 ]", "[ 27/8 ⇜ (11/3 → 15/4) | gain:0.02733603378769718 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5335.806273589214 cutoff:4000 ]", "[ 15/4 → 31/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2568.811347023862 lpattack:0.1 lpenv:2 ftype:24db ]", @@ -7559,44 +7452,44 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 15/4 → 4/1 | gain:0.027475374351507095 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5220.886439234386 cutoff:4000 ]", "[ 15/4 → 4/1 | gain:0.027475374351507095 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5220.886439234386 cutoff:4000 ]", "[ 15/4 → 4/1 | s:hh3 gain:0.7 ]", - "[ (15/4 → 4/1) ⇝ 33/8 | gain:0.014067391667971637 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5220.886439234386 cutoff:4000 ]", - "[ (15/4 → 4/1) ⇝ 33/8 | gain:0.014067391667971637 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5220.886439234386 cutoff:4000 ]", - "[ (15/4 → 4/1) ⇝ 17/4 | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2580.8797353950404 ]", - "[ (15/4 → 4/1) ⇝ 17/4 | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2580.8797353950404 ]", + "[ (15/4 → 4/1) ⇝ 33/8 | gain:0.01407215930427397 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5197.0018638323545 cutoff:4000 ]", + "[ (15/4 → 4/1) ⇝ 33/8 | gain:0.01407215930427397 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5197.0018638323545 cutoff:4000 ]", + "[ (15/4 → 4/1) ⇝ 17/4 | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2604.594154601839 ]", + "[ (15/4 → 4/1) ⇝ 17/4 | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2604.594154601839 ]", "[ 7/2 ⇜ (23/6 → 31/8) | gain:0.017542573009485987 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5290.754858561636 cutoff:4000 ]", "[ 7/2 ⇜ (23/6 → 31/8) | gain:0.017542573009485987 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5290.754858561636 cutoff:4000 ]", "[ 31/8 → 4/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2592.8079367021132 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 31/8 → 4/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2592.8079367021132 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 31/8 → 4/1 | s:bd gain:0.7 ]", - "[ (31/8 → 4/1) ⇝ 17/4 | gain:0.01759019913034246 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5197.0018638323545 cutoff:4000 ]", - "[ (31/8 → 4/1) ⇝ 17/4 | gain:0.01759019913034246 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5197.0018638323545 cutoff:4000 ]", - "[ (31/8 → 4/1) ⇝ 35/8 | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2592.8079367021132 ]", - "[ (31/8 → 4/1) ⇝ 35/8 | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2592.8079367021132 ]", + "[ (31/8 → 4/1) ⇝ 17/4 | gain:0.01759019913034246 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5148.3645377501725 cutoff:4000 ]", + "[ (31/8 → 4/1) ⇝ 17/4 | gain:0.01759019913034246 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5148.3645377501725 cutoff:4000 ]", + "[ (31/8 → 4/1) ⇝ 35/8 | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2627.7335619844803 ]", + "[ (31/8 → 4/1) ⇝ 35/8 | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2627.7335619844803 ]", "[ 29/8 ⇜ (47/12 → 4/1) | gain:0.02196788874761195 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5244.4761496042 cutoff:4000 ]", "[ 29/8 ⇜ (47/12 → 4/1) | gain:0.02196788874761195 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5244.4761496042 cutoff:4000 ]", - "[ 15/4 ⇜ (4/1 → 49/12) ⇝ 33/8 | gain:0.01407215930427397 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5148.3645377501725 cutoff:4000 ]", - "[ 15/4 ⇜ (4/1 → 49/12) ⇝ 33/8 | gain:0.01407215930427397 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5148.3645377501725 cutoff:4000 ]", - "[ 29/8 ⇜ (4/1 → 33/8) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2616.236614133155 ]", - "[ 29/8 ⇜ (4/1 → 33/8) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2616.236614133155 ]", - "[ 31/8 ⇜ (4/1 → 25/6) ⇝ 17/4 | gain:0.017584239584964544 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5123.62012082546 cutoff:4000 ]", - "[ 31/8 ⇜ (4/1 → 25/6) ⇝ 17/4 | gain:0.017584239584964544 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5123.62012082546 cutoff:4000 ]", - "[ 15/4 ⇜ (4/1 → 17/4) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2627.7335619844803 ]", - "[ 15/4 ⇜ (4/1 → 17/4) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2627.7335619844803 ]", + "[ 15/4 ⇜ (4/1 → 49/12) ⇝ 33/8 | gain:0.01407215930427397 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5197.0018638323545 cutoff:4000 ]", + "[ 15/4 ⇜ (4/1 → 49/12) ⇝ 33/8 | gain:0.01407215930427397 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5197.0018638323545 cutoff:4000 ]", + "[ 29/8 ⇜ (4/1 → 33/8) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2580.8797353950404 ]", + "[ 29/8 ⇜ (4/1 → 33/8) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2580.8797353950404 ]", + "[ 31/8 ⇜ (4/1 → 25/6) ⇝ 17/4 | gain:0.01759019913034246 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5148.3645377501725 cutoff:4000 ]", + "[ 31/8 ⇜ (4/1 → 25/6) ⇝ 17/4 | gain:0.01759019913034246 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5148.3645377501725 cutoff:4000 ]", + "[ 15/4 ⇜ (4/1 → 17/4) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2604.594154601839 ]", + "[ 15/4 ⇜ (4/1 → 17/4) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2604.594154601839 ]", "[ 4/1 → 17/4 | gain:0.02198029948120568 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5123.62012082546 cutoff:4000 ]", "[ 4/1 → 17/4 | gain:0.02198029948120568 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5123.62012082546 cutoff:4000 ]", "[ 4/1 → 17/4 | s:bd gain:0.7 ]", "[ (4/1 → 13/3) ⇝ 35/8 | gain:0.011247559038777319 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5098.597504951462 cutoff:4000 ]", "[ (4/1 → 13/3) ⇝ 35/8 | gain:0.011247559038777319 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5098.597504951462 cutoff:4000 ]", - "[ 31/8 ⇜ (4/1 → 35/8) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2639.083266757757 ]", - "[ 31/8 ⇜ (4/1 → 35/8) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2639.083266757757 ]", - "[ 15/4 ⇜ (49/12 → 33/8) | gain:0.01407215930427397 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5148.3645377501725 cutoff:4000 ]", - "[ 15/4 ⇜ (49/12 → 33/8) | gain:0.01407215930427397 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5148.3645377501725 cutoff:4000 ]", + "[ 31/8 ⇜ (4/1 → 35/8) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2627.7335619844803 ]", + "[ 31/8 ⇜ (4/1 → 35/8) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2627.7335619844803 ]", + "[ 15/4 ⇜ (49/12 → 33/8) | gain:0.01407215930427397 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5197.0018638323545 cutoff:4000 ]", + "[ 15/4 ⇜ (49/12 → 33/8) | gain:0.01407215930427397 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5197.0018638323545 cutoff:4000 ]", "[ 33/8 → 17/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2639.083266757757 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 33/8 → 17/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2639.083266757757 lpattack:0.1 lpenv:2 ftype:24db ]", "[ (33/8 → 53/12) ⇝ 9/2 | gain:0.01403405840758879 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5047.734873274585 cutoff:4000 ]", "[ (33/8 → 53/12) ⇝ 9/2 | gain:0.01403405840758879 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5047.734873274585 cutoff:4000 ]", - "[ 31/8 ⇜ (25/6 → 17/4) | gain:0.017584239584964544 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5123.62012082546 cutoff:4000 ]", - "[ 31/8 ⇜ (25/6 → 17/4) | gain:0.017584239584964544 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5123.62012082546 cutoff:4000 ]", + "[ 31/8 ⇜ (25/6 → 17/4) | gain:0.01759019913034246 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5148.3645377501725 cutoff:4000 ]", + "[ 31/8 ⇜ (25/6 → 17/4) | gain:0.01759019913034246 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5148.3645377501725 cutoff:4000 ]", "[ 17/4 → 9/2 | gain:0.01752078272553497 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5021.903572521802 cutoff:4000 ]", "[ 17/4 → 9/2 | gain:0.01752078272553497 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:5021.903572521802 cutoff:4000 ]", "[ 17/4 → 9/2 | s:hh3 gain:0.7 ]", @@ -7629,20 +7522,16 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 19/4 → 5/1 | gain:0.011012190825058119 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4806.246411789873 cutoff:4000 ]", "[ 19/4 → 5/1 | gain:0.011012190825058119 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4806.246411789873 cutoff:4000 ]", "[ 19/4 → 5/1 | s:hh3 gain:0.7 ]", - "[ (19/4 → 5/1) ⇝ 41/8 | gain:0.005619756192058716 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4778.23271519263 cutoff:4000 ]", - "[ (19/4 → 5/1) ⇝ 41/8 | gain:0.005619756192058716 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4778.23271519263 cutoff:4000 ]", + "[ (19/4 → 61/12) ⇝ 41/8 | gain:0.005619756192058716 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4778.23271519263 cutoff:4000 ]", + "[ (19/4 → 61/12) ⇝ 41/8 | gain:0.005619756192058716 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4778.23271519263 cutoff:4000 ]", "[ 9/2 ⇜ (29/6 → 39/8) | gain:0.007107876545841471 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4888.925582549005 cutoff:4000 ]", "[ 9/2 ⇜ (29/6 → 39/8) | gain:0.007107876545841471 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4888.925582549005 cutoff:4000 ]", "[ 39/8 → 5/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2763.195558759784 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 39/8 → 5/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2763.195558759784 lpattack:0.1 lpenv:2 ftype:24db ]", - "[ (39/8 → 5/1) ⇝ 21/4 | gain:0.006973940456445439 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4721.553103742387 cutoff:4000 ]", - "[ (39/8 → 5/1) ⇝ 21/4 | gain:0.006973940456445439 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4721.553103742387 cutoff:4000 ]", + "[ (39/8 → 31/6) ⇝ 21/4 | gain:0.006973940456445439 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4721.553103742387 cutoff:4000 ]", + "[ (39/8 → 31/6) ⇝ 21/4 | gain:0.006973940456445439 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4721.553103742387 cutoff:4000 ]", "[ 37/8 ⇜ (59/12 → 5/1) | gain:0.008836720604435567 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4834.036289789029 cutoff:4000 ]", "[ 37/8 ⇜ (59/12 → 5/1) | gain:0.008836720604435567 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4834.036289789029 cutoff:4000 ]", - "[ 19/4 ⇜ (5/1 → 61/12) ⇝ 41/8 | gain:0.005619756192058716 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4778.23271519263 cutoff:4000 ]", - "[ 19/4 ⇜ (5/1 → 61/12) ⇝ 41/8 | gain:0.005619756192058716 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4778.23271519263 cutoff:4000 ]", - "[ 39/8 ⇜ (5/1 → 31/6) ⇝ 21/4 | gain:0.006973940456445439 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4721.553103742387 cutoff:4000 ]", - "[ 39/8 ⇜ (5/1 → 31/6) ⇝ 21/4 | gain:0.006973940456445439 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4721.553103742387 cutoff:4000 ]", "[ 5/1 → 21/4 | gain:0.008682903916956372 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4692.8969006490215 cutoff:4000 ]", "[ 5/1 → 21/4 | gain:0.008682903916956372 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4692.8969006490215 cutoff:4000 ]", "[ 5/1 → 21/4 | s:bd gain:0.7 ]", @@ -7683,8 +7572,8 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 21/4 ⇜ (67/12 → 45/8) | gain:0.00347470282155788 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4546.64934384357 cutoff:4000 ]", "[ (45/8 → 71/12) ⇝ 6/1 | gain:0.0033534458443527444 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4365.292642693734 cutoff:4000 ]", "[ (45/8 → 71/12) ⇝ 6/1 | gain:0.0033534458443527444 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4365.292642693734 cutoff:4000 ]", - "[ (45/8 → 6/1) ⇝ 49/8 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2877.376777172205 ]", - "[ (45/8 → 6/1) ⇝ 49/8 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2877.376777172205 ]", + "[ (45/8 → 6/1) ⇝ 49/8 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2884.183170199766 ]", + "[ (45/8 → 6/1) ⇝ 49/8 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2884.183170199766 ]", "[ 43/8 ⇜ (17/3 → 23/4) | gain:0.004296220430900771 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4486.859640960669 cutoff:4000 ]", "[ 43/8 ⇜ (17/3 → 23/4) | gain:0.004296220430900771 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4486.859640960669 cutoff:4000 ]", "[ 23/4 → 47/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2877.376777172205 lpattack:0.1 lpenv:2 ftype:24db ]", @@ -7692,43 +7581,43 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 23/4 → 6/1 | gain:0.0041636914909436865 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4334.517148084427 cutoff:4000 ]", "[ 23/4 → 6/1 | gain:0.0041636914909436865 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4334.517148084427 cutoff:4000 ]", "[ 23/4 → 6/1 | s:hh3 gain:0.7 ]", - "[ (23/4 → 6/1) ⇝ 49/8 | gain:0.0021318100433631677 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4334.517148084427 cutoff:4000 ]", - "[ (23/4 → 6/1) ⇝ 49/8 | gain:0.0021318100433631677 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4334.517148084427 cutoff:4000 ]", - "[ (23/4 → 6/1) ⇝ 25/4 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2884.183170199766 ]", - "[ (23/4 → 6/1) ⇝ 25/4 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2884.183170199766 ]", + "[ (23/4 → 6/1) ⇝ 49/8 | gain:0.0021170195539929144 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4303.598663257904 cutoff:4000 ]", + "[ (23/4 → 6/1) ⇝ 49/8 | gain:0.0021170195539929144 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4303.598663257904 cutoff:4000 ]", + "[ (23/4 → 6/1) ⇝ 25/4 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2897.237368890237 ]", + "[ (23/4 → 6/1) ⇝ 25/4 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2897.237368890237 ]", "[ 11/2 ⇜ (35/6 → 47/8) | gain:0.0027172198948820303 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4426.39359377459 cutoff:4000 ]", "[ 11/2 ⇜ (35/6 → 47/8) | gain:0.0027172198948820303 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4426.39359377459 cutoff:4000 ]", "[ 47/8 → 6/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2890.803699781578 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 47/8 → 6/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2890.803699781578 lpattack:0.1 lpenv:2 ftype:24db ]", - "[ (47/8 → 6/1) ⇝ 25/4 | gain:0.002646274442491143 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4303.598663257904 cutoff:4000 ]", - "[ (47/8 → 6/1) ⇝ 25/4 | gain:0.002646274442491143 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4303.598663257904 cutoff:4000 ]", - "[ (47/8 → 6/1) ⇝ 51/8 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2890.803699781578 ]", - "[ (47/8 → 6/1) ⇝ 51/8 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2890.803699781578 ]", + "[ (47/8 → 6/1) ⇝ 25/4 | gain:0.002607861084803616 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4241.3539374389275 cutoff:4000 ]", + "[ (47/8 → 6/1) ⇝ 25/4 | gain:0.002607861084803616 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4241.3539374389275 cutoff:4000 ]", + "[ (47/8 → 6/1) ⇝ 51/8 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2909.5402784268977 ]", + "[ (47/8 → 6/1) ⇝ 51/8 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2909.5402784268977 ]", "[ 45/8 ⇜ (71/12 → 6/1) | gain:0.0033534458443527444 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4365.292642693734 cutoff:4000 ]", "[ 45/8 ⇜ (71/12 → 6/1) | gain:0.0033534458443527444 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4365.292642693734 cutoff:4000 ]", - "[ 23/4 ⇜ (6/1 → 73/12) ⇝ 49/8 | gain:0.002086288867842893 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4241.3539374389275 cutoff:4000 ]", - "[ 23/4 ⇜ (6/1 → 73/12) ⇝ 49/8 | gain:0.002086288867842893 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4241.3539374389275 cutoff:4000 ]", - "[ 45/8 ⇜ (6/1 → 49/8) | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2903.483208638841 ]", - "[ 45/8 ⇜ (6/1 → 49/8) | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2903.483208638841 ]", - "[ 47/8 ⇜ (6/1 → 37/6) ⇝ 25/4 | gain:0.0025879589775992078 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4210.038361759807 cutoff:4000 ]", - "[ 47/8 ⇜ (6/1 → 37/6) ⇝ 25/4 | gain:0.0025879589775992078 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4210.038361759807 cutoff:4000 ]", - "[ 23/4 ⇜ (6/1 → 25/4) | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2909.5402784268977 ]", - "[ 23/4 ⇜ (6/1 → 25/4) | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2909.5402784268977 ]", + "[ 23/4 ⇜ (6/1 → 73/12) ⇝ 49/8 | gain:0.0021170195539929144 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4303.598663257904 cutoff:4000 ]", + "[ 23/4 ⇜ (6/1 → 73/12) ⇝ 49/8 | gain:0.0021170195539929144 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4303.598663257904 cutoff:4000 ]", + "[ 45/8 ⇜ (6/1 → 49/8) | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2884.183170199766 ]", + "[ 45/8 ⇜ (6/1 → 49/8) | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2884.183170199766 ]", + "[ 47/8 ⇜ (6/1 → 37/6) ⇝ 25/4 | gain:0.002607861084803616 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4241.3539374389275 cutoff:4000 ]", + "[ 47/8 ⇜ (6/1 → 37/6) ⇝ 25/4 | gain:0.002607861084803616 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4241.3539374389275 cutoff:4000 ]", + "[ 23/4 ⇜ (6/1 → 25/4) | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2897.237368890237 ]", + "[ 23/4 ⇜ (6/1 → 25/4) | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2897.237368890237 ]", "[ 6/1 → 25/4 | gain:0.0032349487219990097 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4210.038361759807 cutoff:4000 ]", "[ 6/1 → 25/4 | gain:0.0032349487219990097 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4210.038361759807 cutoff:4000 ]", "[ 6/1 → 25/4 | s:bd gain:0.7 ]", "[ (6/1 → 19/3) ⇝ 51/8 | gain:0.0016432698518802527 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4178.601124662687 cutoff:4000 ]", "[ (6/1 → 19/3) ⇝ 51/8 | gain:0.0016432698518802527 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4178.601124662687 cutoff:4000 ]", - "[ 47/8 ⇜ (6/1 → 51/8) | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2915.4076660819765 ]", - "[ 47/8 ⇜ (6/1 → 51/8) | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2915.4076660819765 ]", - "[ 23/4 ⇜ (73/12 → 49/8) | gain:0.002086288867842893 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4241.3539374389275 cutoff:4000 ]", - "[ 23/4 ⇜ (73/12 → 49/8) | gain:0.002086288867842893 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4241.3539374389275 cutoff:4000 ]", + "[ 47/8 ⇜ (6/1 → 51/8) | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2909.5402784268977 ]", + "[ 47/8 ⇜ (6/1 → 51/8) | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2909.5402784268977 ]", + "[ 23/4 ⇜ (73/12 → 49/8) | gain:0.0021170195539929144 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4303.598663257904 cutoff:4000 ]", + "[ 23/4 ⇜ (73/12 → 49/8) | gain:0.0021170195539929144 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4303.598663257904 cutoff:4000 ]", "[ 49/8 → 25/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2915.4076660819765 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 49/8 → 25/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2915.4076660819765 lpattack:0.1 lpenv:2 ftype:24db ]", "[ (49/8 → 77/12) ⇝ 13/2 | gain:0.002020492471376867 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4115.383232572483 cutoff:4000 ]", "[ (49/8 → 77/12) ⇝ 13/2 | gain:0.002020492471376867 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4115.383232572483 cutoff:4000 ]", - "[ 47/8 ⇜ (37/6 → 25/4) | gain:0.0025879589775992078 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4210.038361759807 cutoff:4000 ]", - "[ 47/8 ⇜ (37/6 → 25/4) | gain:0.0025879589775992078 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4210.038361759807 cutoff:4000 ]", + "[ 47/8 ⇜ (37/6 → 25/4) | gain:0.002607861084803616 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4241.3539374389275 cutoff:4000 ]", + "[ 47/8 ⇜ (37/6 → 25/4) | gain:0.002607861084803616 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4241.3539374389275 cutoff:4000 ]", "[ 25/4 → 13/2 | gain:0.0025039971642624803 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4083.6134096397636 cutoff:4000 ]", "[ 25/4 → 13/2 | gain:0.0025039971642624803 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:4083.6134096397636 cutoff:4000 ]", "[ 25/4 → 13/2 | s:hh3 gain:0.7 ]", @@ -7761,20 +7650,16 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 27/4 → 7/1 | gain:0.001483452397136718 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3826.315480550129 cutoff:4000 ]", "[ 27/4 → 7/1 | gain:0.001483452397136718 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3826.315480550129 cutoff:4000 ]", "[ 27/4 → 7/1 | s:hh3 gain:0.7 ]", - "[ (27/4 → 7/1) ⇝ 57/8 | gain:0.0007514349161098732 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3793.8434936445938 cutoff:4000 ]", - "[ (27/4 → 7/1) ⇝ 57/8 | gain:0.0007514349161098732 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3793.8434936445938 cutoff:4000 ]", + "[ (27/4 → 85/12) ⇝ 57/8 | gain:0.0007514349161098732 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3793.8434936445938 cutoff:4000 ]", + "[ (27/4 → 85/12) ⇝ 57/8 | gain:0.0007514349161098732 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3793.8434936445938 cutoff:4000 ]", "[ 13/2 ⇜ (41/6 → 55/8) | gain:0.0009790326438946302 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3923.373759622562 cutoff:4000 ]", "[ 13/2 ⇜ (41/6 → 55/8) | gain:0.0009790326438946302 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3923.373759622562 cutoff:4000 ]", "[ 55/8 → 7/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2970.728450471497 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 55/8 → 7/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2970.728450471497 lpattack:0.1 lpenv:2 ftype:24db ]", - "[ (55/8 → 7/1) ⇝ 29/4 | gain:0.0009187360380160062 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3728.7540466585065 cutoff:4000 ]", - "[ (55/8 → 7/1) ⇝ 29/4 | gain:0.0009187360380160062 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3728.7540466585065 cutoff:4000 ]", + "[ (55/8 → 43/6) ⇝ 29/4 | gain:0.0009187360380160062 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3728.7540466585065 cutoff:4000 ]", + "[ (55/8 → 43/6) ⇝ 29/4 | gain:0.0009187360380160062 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3728.7540466585065 cutoff:4000 ]", "[ 53/8 ⇜ (83/12 → 7/1) | gain:0.0011992608333914556 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3858.7315549779487 cutoff:4000 ]", "[ 53/8 ⇜ (83/12 → 7/1) | gain:0.0011992608333914556 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3858.7315549779487 cutoff:4000 ]", - "[ 27/4 ⇜ (7/1 → 85/12) ⇝ 57/8 | gain:0.0007514349161098732 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3793.8434936445938 cutoff:4000 ]", - "[ 27/4 ⇜ (7/1 → 85/12) ⇝ 57/8 | gain:0.0007514349161098732 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3793.8434936445938 cutoff:4000 ]", - "[ 55/8 ⇜ (7/1 → 43/6) ⇝ 29/4 | gain:0.0009187360380160062 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3728.7540466585065 cutoff:4000 ]", - "[ 55/8 ⇜ (7/1 → 43/6) ⇝ 29/4 | gain:0.0009187360380160062 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3728.7540466585065 cutoff:4000 ]", "[ 7/1 → 29/4 | gain:0.0011353833788233256 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3696.147739319613 cutoff:4000 ]", "[ 7/1 → 29/4 | gain:0.0011353833788233256 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3696.147739319613 cutoff:4000 ]", "[ 7/1 → 29/4 | s:bd gain:0.7 ]", @@ -7815,8 +7700,8 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 29/4 ⇜ (91/12 → 61/8) | gain:0.00043771267437304546 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3532.7239889283615 cutoff:4000 ]", "[ (61/8 → 95/12) ⇝ 8/1 | gain:0.00040393160954279157 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3336.4921769246425 cutoff:4000 ]", "[ (61/8 → 95/12) ⇝ 8/1 | gain:0.00040393160954279157 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3336.4921769246425 cutoff:4000 ]", - "[ (61/8 → 8/1) ⇝ 65/8 | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.0852191942718 ]", - "[ (61/8 → 8/1) ⇝ 65/8 | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.0852191942718 ]", + "[ (61/8 → 8/1) ⇝ 65/8 | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]", + "[ (61/8 → 8/1) ⇝ 65/8 | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]", "[ 59/8 ⇜ (23/3 → 31/4) | gain:0.0005331735858040315 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3467.276011071639 cutoff:4000 ]", "[ 59/8 ⇜ (23/3 → 31/4) | gain:0.0005331735858040315 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3467.276011071639 cutoff:4000 ]", "[ 31/4 → 63/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.0852191942718 lpattack:0.1 lpenv:2 ftype:24db ]", @@ -7824,44 +7709,44 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 31/4 → 8/1 | gain:0.0004978069306625383 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3303.852260680389 cutoff:4000 ]", "[ 31/4 → 8/1 | gain:0.0004978069306625383 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3303.852260680389 cutoff:4000 ]", "[ 31/4 → 8/1 | s:hh3 gain:0.7 ]", - "[ (31/4 → 8/1) ⇝ 65/8 | gain:0.00025487714849921963 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3303.852260680389 cutoff:4000 ]", - "[ (31/4 → 8/1) ⇝ 65/8 | gain:0.00025487714849921963 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3303.852260680389 cutoff:4000 ]", - "[ (31/4 → 8/1) ⇝ 33/4 | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]", - "[ (31/4 → 8/1) ⇝ 33/4 | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]", + "[ (31/4 → 8/1) ⇝ 65/8 | gain:0.0002512336761852884 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3271.2459533414954 cutoff:4000 ]", + "[ (31/4 → 8/1) ⇝ 65/8 | gain:0.0002512336761852884 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3271.2459533414954 cutoff:4000 ]", + "[ (31/4 → 8/1) ⇝ 33/4 | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:3000 ]", + "[ (31/4 → 8/1) ⇝ 33/4 | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:3000 ]", "[ 15/2 ⇜ (47/6 → 63/8) | gain:0.0003322155712311059 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3401.8504606023293 cutoff:4000 ]", "[ 15/2 ⇜ (47/6 → 63/8) | gain:0.0003322155712311059 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3401.8504606023293 cutoff:4000 ]", "[ 63/8 → 8/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.898347482845 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 63/8 → 8/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.898347482845 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 63/8 → 8/1 | s:bd gain:0.7 ]", - "[ (63/8 → 8/1) ⇝ 33/4 | gain:0.00031404209523161047 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3271.2459533414954 cutoff:4000 ]", - "[ (63/8 → 8/1) ⇝ 33/4 | gain:0.00031404209523161047 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3271.2459533414954 cutoff:4000 ]", - "[ (63/8 → 8/1) ⇝ 67/8 | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.898347482845 ]", - "[ (63/8 → 8/1) ⇝ 67/8 | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.898347482845 ]", + "[ (63/8 → 8/1) ⇝ 33/4 | gain:0.0003049279244110808 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3206.156506355406 cutoff:4000 ]", + "[ (63/8 → 8/1) ⇝ 33/4 | gain:0.0003049279244110808 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3206.156506355406 cutoff:4000 ]", + "[ (63/8 → 8/1) ⇝ 67/8 | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]", + "[ (63/8 → 8/1) ⇝ 67/8 | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]", "[ 61/8 ⇜ (95/12 → 8/1) | gain:0.00040393160954279157 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3336.4921769246425 cutoff:4000 ]", "[ 61/8 ⇜ (95/12 → 8/1) | gain:0.00040393160954279157 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3336.4921769246425 cutoff:4000 ]", - "[ 31/4 ⇜ (8/1 → 97/12) ⇝ 65/8 | gain:0.00024394233952886466 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3206.156506355406 cutoff:4000 ]", - "[ 31/4 ⇜ (8/1 → 97/12) ⇝ 65/8 | gain:0.00024394233952886466 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3206.156506355406 cutoff:4000 ]", - "[ 61/8 ⇜ (8/1 → 65/8) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.898347482845 ]", - "[ 61/8 ⇜ (8/1 → 65/8) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.898347482845 ]", - "[ 63/8 ⇜ (8/1 → 49/6) ⇝ 33/4 | gain:0.0003003735840186667 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3173.6845194498705 cutoff:4000 ]", - "[ 63/8 ⇜ (8/1 → 49/6) ⇝ 33/4 | gain:0.0003003735840186667 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3173.6845194498705 cutoff:4000 ]", - "[ 31/4 ⇜ (8/1 → 33/4) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]", - "[ 31/4 ⇜ (8/1 → 33/4) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]", + "[ 31/4 ⇜ (8/1 → 97/12) ⇝ 65/8 | gain:0.0002512336761852884 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3271.2459533414954 cutoff:4000 ]", + "[ 31/4 ⇜ (8/1 → 97/12) ⇝ 65/8 | gain:0.0002512336761852884 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3271.2459533414954 cutoff:4000 ]", + "[ 61/8 ⇜ (8/1 → 65/8) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]", + "[ 61/8 ⇜ (8/1 → 65/8) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]", + "[ 63/8 ⇜ (8/1 → 49/6) ⇝ 33/4 | gain:0.0003049279244110808 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3206.156506355406 cutoff:4000 ]", + "[ 63/8 ⇜ (8/1 → 49/6) ⇝ 33/4 | gain:0.0003049279244110808 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3206.156506355406 cutoff:4000 ]", + "[ 31/4 ⇜ (8/1 → 33/4) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:3000 ]", + "[ 31/4 ⇜ (8/1 → 33/4) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:3000 ]", "[ 8/1 → 33/4 | gain:0.0003754669800233334 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3173.6845194498705 cutoff:4000 ]", "[ 8/1 → 33/4 | gain:0.0003754669800233334 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3173.6845194498705 cutoff:4000 ]", "[ 8/1 → 33/4 | s:bd gain:0.7 ]", "[ (8/1 → 25/3) ⇝ 67/8 | gain:0.2389653154600499 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3141.2684450220513 cutoff:4000 ]", "[ (8/1 → 25/3) ⇝ 67/8 | gain:0.2389653154600499 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3141.2684450220513 cutoff:4000 ]", - "[ 63/8 ⇜ (8/1 → 67/8) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.0852191942718 ]", - "[ 63/8 ⇜ (8/1 → 67/8) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.0852191942718 ]", - "[ 31/4 ⇜ (97/12 → 65/8) | gain:0.00024394233952886466 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3206.156506355406 cutoff:4000 ]", - "[ 31/4 ⇜ (97/12 → 65/8) | gain:0.00024394233952886466 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3206.156506355406 cutoff:4000 ]", + "[ 63/8 ⇜ (8/1 → 67/8) | gain:0.7 note:G3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]", + "[ 63/8 ⇜ (8/1 → 67/8) | gain:0.7 note:B3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]", + "[ 31/4 ⇜ (97/12 → 65/8) | gain:0.0002512336761852884 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3271.2459533414954 cutoff:4000 ]", + "[ 31/4 ⇜ (97/12 → 65/8) | gain:0.0002512336761852884 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3271.2459533414954 cutoff:4000 ]", "[ 65/8 → 33/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.0852191942718 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 65/8 → 33/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.0852191942718 lpattack:0.1 lpenv:2 ftype:24db ]", "[ (65/8 → 101/12) ⇝ 17/2 | gain:0.00022940355872926835 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3076.6262403774385 cutoff:4000 ]", "[ (65/8 → 101/12) ⇝ 17/2 | gain:0.00022940355872926835 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3076.6262403774385 cutoff:4000 ]", - "[ 63/8 ⇜ (49/6 → 33/4) | gain:0.0003003735840186667 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3173.6845194498705 cutoff:4000 ]", - "[ 63/8 ⇜ (49/6 → 33/4) | gain:0.0003003735840186667 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3173.6845194498705 cutoff:4000 ]", + "[ 63/8 ⇜ (49/6 → 33/4) | gain:0.0003049279244110808 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3206.156506355406 cutoff:4000 ]", + "[ 63/8 ⇜ (49/6 → 33/4) | gain:0.0003049279244110808 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3206.156506355406 cutoff:4000 ]", "[ 33/4 → 17/2 | gain:0.00028223848042460063 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3044.4111862696313 cutoff:4000 ]", "[ 33/4 → 17/2 | gain:0.00028223848042460063 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:3044.4111862696313 cutoff:4000 ]", "[ 33/4 → 17/2 | s:hh3 gain:0.7 ]", @@ -7894,20 +7779,16 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 35/4 → 9/1 | gain:0.19946652199116702 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2789.9616382401937 cutoff:4000 ]", "[ 35/4 → 9/1 | gain:0.19946652199116702 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2789.9616382401937 cutoff:4000 ]", "[ 35/4 → 9/1 | s:hh3 gain:0.7 ]", - "[ (35/4 → 9/1) ⇝ 73/8 | gain:0.10036006119411293 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2758.6460625610725 cutoff:4000 ]", - "[ (35/4 → 9/1) ⇝ 73/8 | gain:0.10036006119411293 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2758.6460625610725 cutoff:4000 ]", + "[ (35/4 → 109/12) ⇝ 73/8 | gain:0.10036006119411293 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2758.6460625610725 cutoff:4000 ]", + "[ (35/4 → 109/12) ⇝ 73/8 | gain:0.10036006119411293 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2758.6460625610725 cutoff:4000 ]", "[ 17/2 ⇜ (53/6 → 71/8) | gain:0.1343955752824098 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2884.6167674275184 cutoff:4000 ]", "[ 17/2 ⇜ (53/6 → 71/8) | gain:0.1343955752824098 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2884.6167674275184 cutoff:4000 ]", "[ 71/8 → 9/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2977.1924080321423 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 71/8 → 9/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2977.1924080321423 lpattack:0.1 lpenv:2 ftype:24db ]", - "[ (71/8 → 9/1) ⇝ 37/4 | gain:0.12109683385552102 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2696.4013367420957 cutoff:4000 ]", - "[ (71/8 → 9/1) ⇝ 37/4 | gain:0.12109683385552102 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2696.4013367420957 cutoff:4000 ]", + "[ (71/8 → 55/6) ⇝ 37/4 | gain:0.12109683385552102 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2696.4013367420957 cutoff:4000 ]", + "[ (71/8 → 55/6) ⇝ 37/4 | gain:0.12109683385552102 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2696.4013367420957 cutoff:4000 ]", "[ 69/8 ⇜ (107/12 → 9/1) | gain:0.16235819115213307 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2821.398875337315 cutoff:4000 ]", "[ 69/8 ⇜ (107/12 → 9/1) | gain:0.16235819115213307 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2821.398875337315 cutoff:4000 ]", - "[ 35/4 ⇜ (9/1 → 109/12) ⇝ 73/8 | gain:0.10036006119411293 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2758.6460625610725 cutoff:4000 ]", - "[ 35/4 ⇜ (9/1 → 109/12) ⇝ 73/8 | gain:0.10036006119411293 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2758.6460625610725 cutoff:4000 ]", - "[ 71/8 ⇜ (9/1 → 55/6) ⇝ 37/4 | gain:0.12109683385552102 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2696.4013367420957 cutoff:4000 ]", - "[ 71/8 ⇜ (9/1 → 55/6) ⇝ 37/4 | gain:0.12109683385552102 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2696.4013367420957 cutoff:4000 ]", "[ 9/1 → 37/4 | gain:0.1486933887883662 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2665.4828519155726 cutoff:4000 ]", "[ 9/1 → 37/4 | gain:0.1486933887883662 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2665.4828519155726 cutoff:4000 ]", "[ 9/1 → 37/4 | s:bd gain:0.7 ]", @@ -7948,8 +7829,8 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 37/4 ⇜ (115/12 → 77/8) | gain:0.05562379698730943 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2513.140359039332 cutoff:4000 ]", "[ (77/8 → 119/12) ⇝ 10/1 | gain:0.04981524842313599 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2335.9636991872226 cutoff:4000 ]", "[ (77/8 → 119/12) ⇝ 10/1 | gain:0.04981524842313599 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2335.9636991872226 cutoff:4000 ]", - "[ (77/8 → 10/1) ⇝ 81/8 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2915.4076660819765 ]", - "[ (77/8 → 10/1) ⇝ 81/8 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2915.4076660819765 ]", + "[ (77/8 → 10/1) ⇝ 81/8 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2909.5402784268977 ]", + "[ (77/8 → 10/1) ⇝ 81/8 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2909.5402784268977 ]", "[ 75/8 ⇜ (29/3 → 39/4) | gain:0.0670223447192876 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2453.350656156431 cutoff:4000 ]", "[ 75/8 ⇜ (29/3 → 39/4) | gain:0.0670223447192876 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2453.350656156431 cutoff:4000 ]", "[ 39/4 → 79/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2915.4076660819765 lpattack:0.1 lpenv:2 ftype:24db ]", @@ -7957,18 +7838,18 @@ exports[`renders tunes > tune: hyperpop 1`] = ` "[ 39/4 → 10/1 | gain:0.0611394178141992 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2307.1030993509794 cutoff:4000 ]", "[ 39/4 → 10/1 | gain:0.0611394178141992 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2307.1030993509794 cutoff:4000 ]", "[ 39/4 → 10/1 | s:hh3 gain:0.7 ]", - "[ (39/4 → 10/1) ⇝ 81/8 | gain:0.031303381920869996 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2307.1030993509794 cutoff:4000 ]", - "[ (39/4 → 10/1) ⇝ 81/8 | gain:0.031303381920869996 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2307.1030993509794 cutoff:4000 ]", - "[ (39/4 → 10/1) ⇝ 41/4 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2909.5402784268977 ]", - "[ (39/4 → 10/1) ⇝ 41/4 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2909.5402784268977 ]", + "[ (39/4 → 10/1) ⇝ 81/8 | gain:0.030737730012853577 note:C#6 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2278.446896257612 cutoff:4000 ]", + "[ (39/4 → 10/1) ⇝ 81/8 | gain:0.030737730012853577 note:E5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2278.446896257612 cutoff:4000 ]", + "[ (39/4 → 10/1) ⇝ 41/4 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2897.237368890237 ]", + "[ (39/4 → 10/1) ⇝ 41/4 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2897.237368890237 ]", "[ 19/2 ⇜ (59/6 → 79/8) | gain:0.04134410948782485 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2394.2782744524975 cutoff:4000 ]", "[ 19/2 ⇜ (59/6 → 79/8) | gain:0.04134410948782485 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2394.2782744524975 cutoff:4000 ]", "[ 79/8 → 10/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2903.483208638841 lpattack:0.1 lpenv:2 ftype:24db ]", "[ 79/8 → 10/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2903.483208638841 lpattack:0.1 lpenv:2 ftype:24db ]", - "[ (79/8 → 10/1) ⇝ 41/4 | gain:0.03842216251606697 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2278.446896257612 cutoff:4000 ]", - "[ (79/8 → 10/1) ⇝ 41/4 | gain:0.03842216251606697 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2278.446896257612 cutoff:4000 ]", - "[ (79/8 → 10/1) ⇝ 83/8 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2903.483208638841 ]", - "[ (79/8 → 10/1) ⇝ 83/8 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2903.483208638841 ]", + "[ (79/8 → 10/1) ⇝ 41/4 | gain:0.03705744590301562 note:A5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2221.7672848073694 cutoff:4000 ]", + "[ (79/8 → 10/1) ⇝ 41/4 | gain:0.03705744590301562 note:C#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2221.7672848073694 cutoff:4000 ]", + "[ (79/8 → 10/1) ⇝ 83/8 | gain:0.7 note:F#3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2884.183170199766 ]", + "[ (79/8 → 10/1) ⇝ 83/8 | gain:0.7 note:A3 s:square attack:0.01 decay:0.1 sustain:0 cutoff:2884.183170199766 ]", "[ 77/8 ⇜ (119/12 → 10/1) | gain:0.04981524842313599 note:F#5 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2335.9636991872226 cutoff:4000 ]", "[ 77/8 ⇜ (119/12 → 10/1) | gain:0.04981524842313599 note:A4 s:sawtooth attack:0.001 decay:0.2 sustain:0 hcutoff:2335.9636991872226 cutoff:4000 ]", ] @@ -7982,8 +7863,8 @@ exports[`renders tunes > tune: juxUndTollerei 1`] = ` "[ 1/4 → 1/2 | note:g3 s:sawtooth pan:1 cutoff:1361.2562095290161 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 1/2 → 3/4 | note:g3 s:sawtooth pan:0 cutoff:1524.257063143398 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 1/2 → 3/4 | note:eb3 s:sawtooth pan:1 cutoff:1524.257063143398 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", - "[ (101/200 → 1/1) ⇝ 201/200 | note:55 s:triangle pan:0 cutoff:1601.4815730092653 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", - "[ (101/200 → 1/1) ⇝ 201/200 | note:65 s:triangle pan:1 cutoff:1601.4815730092653 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", + "[ (101/200 → 1/1) ⇝ 201/200 | note:55 s:triangle pan:0 cutoff:1602.9480029324704 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", + "[ (101/200 → 1/1) ⇝ 201/200 | note:65 s:triangle pan:1 cutoff:1602.9480029324704 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 3/4 → 1/1 | note:bb3 s:sawtooth pan:0 cutoff:1670.953955747281 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 3/4 → 1/1 | note:c3 s:sawtooth pan:1 cutoff:1670.953955747281 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", ] @@ -7992,27 +7873,27 @@ exports[`renders tunes > tune: juxUndTollerei 1`] = ` exports[`renders tunes > tune: loungeSponge 1`] = ` [ "[ 0/1 → 1/4 | note:C2 gain:1 ]", - "[ 0/1 → 3/8 | note:B3 cutoff:1396 ]", - "[ 0/1 → 3/8 | note:D4 cutoff:1396 ]", - "[ 0/1 → 3/8 | note:E4 cutoff:1396 ]", - "[ 0/1 → 3/8 | note:G4 cutoff:1396 ]", + "[ 0/1 → 3/8 | note:B3 cutoff:1537 ]", + "[ 0/1 → 3/8 | note:D4 cutoff:1537 ]", + "[ 0/1 → 3/8 | note:E4 cutoff:1537 ]", + "[ 0/1 → 3/8 | note:G4 cutoff:1537 ]", "[ -1/4 ⇜ (0/1 → 1/2) | n:E5 clip:0.25 ]", "[ 0/1 → 1/2 | s:bd bank:RolandTR909 ]", "[ 0/1 → 3/4 | n:A4 clip:0.25 ]", "[ 1/4 → 1/2 | note:C2 gain:4 ]", "[ 1/4 → 1/2 | s:hh bank:RolandTR909 ]", - "[ 3/8 → 3/4 | note:B3 cutoff:1396 ]", - "[ 3/8 → 3/4 | note:D4 cutoff:1396 ]", - "[ 3/8 → 3/4 | note:E4 cutoff:1396 ]", - "[ 3/8 → 3/4 | note:G4 cutoff:1396 ]", + "[ 3/8 → 3/4 | note:B3 cutoff:1537 ]", + "[ 3/8 → 3/4 | note:D4 cutoff:1537 ]", + "[ 3/8 → 3/4 | note:E4 cutoff:1537 ]", + "[ 3/8 → 3/4 | note:G4 cutoff:1537 ]", "[ 1/2 → 3/4 | note:C2 gain:1 ]", "[ 1/2 → 1/1 | s:bd bank:RolandTR909 ]", "[ 1/2 → 1/1 | s:cp bank:RolandTR909 ]", "[ (1/2 → 1/1) ⇝ 5/4 | n:C5 clip:0.25 ]", - "[ 3/4 → 1/1 | note:B3 cutoff:1396 ]", - "[ 3/4 → 1/1 | note:D4 cutoff:1396 ]", - "[ 3/4 → 1/1 | note:E4 cutoff:1396 ]", - "[ 3/4 → 1/1 | note:G4 cutoff:1396 ]", + "[ 3/4 → 1/1 | note:B3 cutoff:1537 ]", + "[ 3/4 → 1/1 | note:D4 cutoff:1537 ]", + "[ 3/4 → 1/1 | note:E4 cutoff:1537 ]", + "[ 3/4 → 1/1 | note:G4 cutoff:1537 ]", "[ 3/4 → 1/1 | note:C2 gain:4 ]", "[ 3/4 → 1/1 | s:hh bank:RolandTR909 ]", "[ (3/4 → 1/1) ⇝ 3/2 | n:A5 clip:0.25 ]", @@ -8087,146 +7968,74 @@ exports[`renders tunes > tune: orbit 1`] = ` exports[`renders tunes > tune: randomBells 1`] = ` [ - "[ -9/8 ⇜ (0/1 → 3/8) | gain:0.6 note:G4 velocity:0.7893480537459254 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ -3/4 ⇜ (0/1 → 3/4) | gain:0.6 note:F5 velocity:0.9213038925081491 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (0/1 → 1/1) ⇝ 3/2 | note:D2 s:bass clip:1 gain:0.8 ]", - "[ (0/1 → 1/1) ⇝ 9/4 | gain:0.6 note:A3 velocity:0.6003328701481223 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (3/8 → 1/1) ⇝ 21/8 | gain:0.6 note:D4 velocity:0.6848798459395766 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (3/4 → 1/1) ⇝ 3/1 | gain:0.6 note:G4 velocity:0.7837830819189548 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 0/1 ⇜ (1/1 → 3/2) | note:D2 s:bass clip:1 gain:0.8 ]", - "[ 0/1 ⇜ (1/1 → 2/1) ⇝ 9/4 | gain:0.6 note:A3 velocity:0.6003328701481223 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 3/8 ⇜ (1/1 → 2/1) ⇝ 21/8 | gain:0.6 note:D4 velocity:0.6848798459395766 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 3/4 ⇜ (1/1 → 2/1) ⇝ 3/1 | gain:0.6 note:G4 velocity:0.7837830819189548 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (3/2 → 2/1) ⇝ 9/4 | note:D2 s:bass clip:1 gain:0.8 ]", - "[ 0/1 ⇜ (2/1 → 9/4) | gain:0.6 note:A3 velocity:0.6003328701481223 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 3/2 ⇜ (2/1 → 9/4) | note:D2 s:bass clip:1 gain:0.8 ]", - "[ 3/8 ⇜ (2/1 → 21/8) | gain:0.6 note:D4 velocity:0.6848798459395766 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 3/4 ⇜ (2/1 → 3/1) | gain:0.6 note:G4 velocity:0.7837830819189548 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ -9/8 ⇜ (0/1 → 3/8) | gain:0.6 note:F5 velocity:0.9184964690357447 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ -3/4 ⇜ (0/1 → 3/4) | gain:0.6 note:D3 velocity:0.5 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 0/1 → 3/2 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 0/1 → 9/4 | gain:0.6 note:A3 velocity:0.6003328701481223 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 3/8 → 21/8 | gain:0.6 note:D4 velocity:0.6848798459395766 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 3/4 → 3/1 | gain:0.6 note:G4 velocity:0.7837830819189548 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 3/2 → 9/4 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 9/4 → 3/1 | note:D2 s:bass clip:1 gain:0.8 ]", - "[ (9/4 → 3/1) ⇝ 9/2 | gain:0.6 note:G3 velocity:0.5819958923384547 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (21/8 → 3/1) ⇝ 39/8 | gain:0.6 note:G3 velocity:0.567817933857441 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 9/4 ⇜ (3/1 → 4/1) ⇝ 9/2 | gain:0.6 note:G3 velocity:0.5819958923384547 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 21/8 ⇜ (3/1 → 4/1) ⇝ 39/8 | gain:0.6 note:G3 velocity:0.567817933857441 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (3/1 → 4/1) ⇝ 9/2 | note:D2 s:bass clip:1 gain:0.8 ]", - "[ (3/1 → 4/1) ⇝ 21/4 | gain:0.6 note:D4 velocity:0.704405858181417 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 9/4 ⇜ (4/1 → 9/2) | gain:0.6 note:G3 velocity:0.5819958923384547 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 3/1 ⇜ (4/1 → 9/2) | note:D2 s:bass clip:1 gain:0.8 ]", - "[ 21/8 ⇜ (4/1 → 39/8) | gain:0.6 note:G3 velocity:0.567817933857441 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 3/1 ⇜ (4/1 → 5/1) ⇝ 21/4 | gain:0.6 note:D4 velocity:0.704405858181417 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (9/2 → 5/1) ⇝ 21/4 | note:D2 s:bass clip:1 gain:0.8 ]", - "[ (9/2 → 5/1) ⇝ 6/1 | gain:0.6 note:D4 velocity:0.6988155404105783 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (39/8 → 5/1) ⇝ 51/8 | gain:0.6 note:C5 velocity:0.867275302298367 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 3/1 ⇜ (5/1 → 21/4) | gain:0.6 note:D4 velocity:0.704405858181417 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 9/2 ⇜ (5/1 → 21/4) | note:D2 s:bass clip:1 gain:0.8 ]", - "[ 9/2 ⇜ (5/1 → 6/1) | gain:0.6 note:D4 velocity:0.6988155404105783 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 39/8 ⇜ (5/1 → 6/1) ⇝ 51/8 | gain:0.6 note:C5 velocity:0.867275302298367 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 9/4 → 9/2 | gain:0.6 note:G3 velocity:0.5819958923384547 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 21/8 → 39/8 | gain:0.6 note:G3 velocity:0.567817933857441 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 3/1 → 9/2 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 3/1 → 21/4 | gain:0.6 note:D4 velocity:0.704405858181417 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 9/2 → 21/4 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 9/2 → 6/1 | gain:0.6 note:D4 velocity:0.6988155404105783 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ (39/8 → 6/1) ⇝ 51/8 | gain:0.6 note:C5 velocity:0.8514789454638958 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 21/4 → 6/1 | note:D2 s:bass clip:1 gain:0.8 ]", - "[ (21/4 → 6/1) ⇝ 27/4 | gain:0.6 note:C5 velocity:0.8514789454638958 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 39/8 ⇜ (6/1 → 51/8) | gain:0.6 note:G3 velocity:0.5832531340420246 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 21/4 ⇜ (6/1 → 27/4) | gain:0.6 note:G4 velocity:0.7743164440616965 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (6/1 → 7/1) ⇝ 15/2 | note:A2 s:bass clip:1 gain:0.8 ]", - "[ (6/1 → 7/1) ⇝ 33/4 | gain:0.6 note:F3 velocity:0.5408733254298568 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (51/8 → 7/1) ⇝ 69/8 | gain:0.6 note:C5 velocity:0.8643641015514731 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (27/4 → 7/1) ⇝ 9/1 | gain:0.6 note:F3 velocity:0.5405213935300708 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 6/1 ⇜ (7/1 → 15/2) | note:A2 s:bass clip:1 gain:0.8 ]", - "[ 6/1 ⇜ (7/1 → 8/1) ⇝ 33/4 | gain:0.6 note:F3 velocity:0.5408733254298568 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 51/8 ⇜ (7/1 → 8/1) ⇝ 69/8 | gain:0.6 note:C5 velocity:0.8643641015514731 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 27/4 ⇜ (7/1 → 8/1) ⇝ 9/1 | gain:0.6 note:F3 velocity:0.5405213935300708 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (15/2 → 8/1) ⇝ 33/4 | note:A2 s:bass clip:1 gain:0.8 ]", - "[ 6/1 ⇜ (8/1 → 33/4) | gain:0.6 note:F3 velocity:0.5408733254298568 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 15/2 ⇜ (8/1 → 33/4) | note:A2 s:bass clip:1 gain:0.8 ]", - "[ 51/8 ⇜ (8/1 → 69/8) | gain:0.6 note:C5 velocity:0.8643641015514731 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 27/4 ⇜ (8/1 → 9/1) | gain:0.6 note:F3 velocity:0.5405213935300708 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ (21/4 → 6/1) ⇝ 27/4 | gain:0.6 note:G4 velocity:0.7597710825502872 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 39/8 ⇜ (6/1 → 51/8) | gain:0.6 note:C5 velocity:0.8514789454638958 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 21/4 ⇜ (6/1 → 27/4) | gain:0.6 note:G4 velocity:0.7597710825502872 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 6/1 → 15/2 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 6/1 → 33/4 | gain:0.6 note:F3 velocity:0.5408733254298568 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 51/8 → 69/8 | gain:0.6 note:C5 velocity:0.8643641015514731 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 27/4 → 9/1 | gain:0.6 note:F3 velocity:0.5405213935300708 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 15/2 → 33/4 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 33/4 → 9/1 | note:A2 s:bass clip:1 gain:0.8 ]", - "[ (33/4 → 9/1) ⇝ 21/2 | gain:0.6 note:A3 velocity:0.5854638321325183 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (69/8 → 9/1) ⇝ 87/8 | gain:0.6 note:F4 velocity:0.7483773557469249 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 33/4 ⇜ (9/1 → 10/1) ⇝ 21/2 | gain:0.6 note:A3 velocity:0.5854638321325183 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 69/8 ⇜ (9/1 → 10/1) ⇝ 87/8 | gain:0.6 note:F4 velocity:0.7483773557469249 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (9/1 → 10/1) ⇝ 21/2 | note:A2 s:bass clip:1 gain:0.8 ]", - "[ (9/1 → 10/1) ⇝ 45/4 | gain:0.6 note:C4 velocity:0.6479453053325415 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 33/4 ⇜ (10/1 → 21/2) | gain:0.6 note:A3 velocity:0.5854638321325183 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 9/1 ⇜ (10/1 → 21/2) | note:A2 s:bass clip:1 gain:0.8 ]", - "[ 69/8 ⇜ (10/1 → 87/8) | gain:0.6 note:F4 velocity:0.7483773557469249 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 9/1 ⇜ (10/1 → 11/1) ⇝ 45/4 | gain:0.6 note:C4 velocity:0.6479453053325415 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (21/2 → 11/1) ⇝ 45/4 | note:A2 s:bass clip:1 gain:0.8 ]", - "[ (21/2 → 11/1) ⇝ 12/1 | gain:0.6 note:A4 velocity:0.7972785895690322 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (87/8 → 11/1) ⇝ 99/8 | gain:0.6 note:F4 velocity:0.7249447042122483 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 9/1 ⇜ (11/1 → 45/4) | gain:0.6 note:C4 velocity:0.6479453053325415 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 21/2 ⇜ (11/1 → 45/4) | note:A2 s:bass clip:1 gain:0.8 ]", - "[ 21/2 ⇜ (11/1 → 12/1) | gain:0.6 note:A4 velocity:0.7972785895690322 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 87/8 ⇜ (11/1 → 12/1) ⇝ 99/8 | gain:0.6 note:F4 velocity:0.7249447042122483 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 33/4 → 21/2 | gain:0.6 note:A3 velocity:0.5854638321325183 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 69/8 → 87/8 | gain:0.6 note:F4 velocity:0.7483773557469249 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 9/1 → 21/2 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 9/1 → 45/4 | gain:0.6 note:C4 velocity:0.6479453053325415 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 21/2 → 45/4 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 21/2 → 12/1 | gain:0.6 note:A4 velocity:0.7972785895690322 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ (87/8 → 12/1) ⇝ 99/8 | gain:0.6 note:F5 velocity:0.9336296319961548 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 45/4 → 12/1 | note:A2 s:bass clip:1 gain:0.8 ]", - "[ (45/4 → 12/1) ⇝ 51/4 | gain:0.6 note:F5 velocity:0.9336296319961548 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 87/8 ⇜ (12/1 → 99/8) | gain:0.6 note:C5 velocity:0.8424309445545077 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 45/4 ⇜ (12/1 → 51/4) | gain:0.6 note:C4 velocity:0.6662392104044557 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (12/1 → 13/1) ⇝ 27/2 | note:G2 s:bass clip:1 gain:0.8 ]", - "[ (12/1 → 13/1) ⇝ 57/4 | gain:0.6 note:F3 velocity:0.5185003904625773 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (99/8 → 13/1) ⇝ 117/8 | gain:0.6 note:D4 velocity:0.67274125572294 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (51/4 → 13/1) ⇝ 15/1 | gain:0.6 note:A4 velocity:0.8324771635234356 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 12/1 ⇜ (13/1 → 27/2) | note:G2 s:bass clip:1 gain:0.8 ]", - "[ 12/1 ⇜ (13/1 → 14/1) ⇝ 57/4 | gain:0.6 note:F3 velocity:0.5185003904625773 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 99/8 ⇜ (13/1 → 14/1) ⇝ 117/8 | gain:0.6 note:D4 velocity:0.67274125572294 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 51/4 ⇜ (13/1 → 14/1) ⇝ 15/1 | gain:0.6 note:A4 velocity:0.8324771635234356 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (27/2 → 14/1) ⇝ 57/4 | note:G2 s:bass clip:1 gain:0.8 ]", - "[ 12/1 ⇜ (14/1 → 57/4) | gain:0.6 note:F3 velocity:0.5185003904625773 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 27/2 ⇜ (14/1 → 57/4) | note:G2 s:bass clip:1 gain:0.8 ]", - "[ 99/8 ⇜ (14/1 → 117/8) | gain:0.6 note:D4 velocity:0.67274125572294 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 51/4 ⇜ (14/1 → 15/1) | gain:0.6 note:A4 velocity:0.8324771635234356 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ (45/4 → 12/1) ⇝ 51/4 | gain:0.6 note:G5 velocity:0.9797635599970818 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 87/8 ⇜ (12/1 → 99/8) | gain:0.6 note:F5 velocity:0.9336296319961548 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 45/4 ⇜ (12/1 → 51/4) | gain:0.6 note:G5 velocity:0.9797635599970818 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 12/1 → 27/2 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 12/1 → 57/4 | gain:0.6 note:F3 velocity:0.5185003904625773 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 99/8 → 117/8 | gain:0.6 note:D4 velocity:0.67274125572294 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 51/4 → 15/1 | gain:0.6 note:A4 velocity:0.8324771635234356 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 27/2 → 57/4 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 57/4 → 15/1 | note:G2 s:bass clip:1 gain:0.8 ]", - "[ (57/4 → 15/1) ⇝ 33/2 | gain:0.6 note:A4 velocity:0.803433682769537 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (117/8 → 15/1) ⇝ 135/8 | gain:0.6 note:G3 velocity:0.5800967421382666 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 57/4 ⇜ (15/1 → 16/1) ⇝ 33/2 | gain:0.6 note:A4 velocity:0.803433682769537 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 117/8 ⇜ (15/1 → 16/1) ⇝ 135/8 | gain:0.6 note:G3 velocity:0.5800967421382666 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (15/1 → 16/1) ⇝ 33/2 | note:G2 s:bass clip:1 gain:0.8 ]", - "[ (15/1 → 16/1) ⇝ 69/4 | gain:0.6 note:G4 velocity:0.769017674960196 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 57/4 ⇜ (16/1 → 33/2) | gain:0.6 note:A4 velocity:0.803433682769537 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 15/1 ⇜ (16/1 → 33/2) | note:G2 s:bass clip:1 gain:0.8 ]", - "[ 117/8 ⇜ (16/1 → 135/8) | gain:0.6 note:G3 velocity:0.5800967421382666 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 15/1 ⇜ (16/1 → 17/1) ⇝ 69/4 | gain:0.6 note:G4 velocity:0.769017674960196 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (33/2 → 17/1) ⇝ 69/4 | note:G2 s:bass clip:1 gain:0.8 ]", - "[ (33/2 → 17/1) ⇝ 18/1 | gain:0.6 note:F3 velocity:0.5081270858645439 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (135/8 → 17/1) ⇝ 147/8 | gain:0.6 note:F4 velocity:0.7109030662104487 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 15/1 ⇜ (17/1 → 69/4) | gain:0.6 note:G4 velocity:0.769017674960196 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 33/2 ⇜ (17/1 → 69/4) | note:G2 s:bass clip:1 gain:0.8 ]", - "[ 33/2 ⇜ (17/1 → 18/1) | gain:0.6 note:F3 velocity:0.5081270858645439 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 135/8 ⇜ (17/1 → 18/1) ⇝ 147/8 | gain:0.6 note:F4 velocity:0.7109030662104487 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 57/4 → 33/2 | gain:0.6 note:A4 velocity:0.803433682769537 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 117/8 → 135/8 | gain:0.6 note:G3 velocity:0.5800967421382666 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 15/1 → 33/2 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 15/1 → 69/4 | gain:0.6 note:G4 velocity:0.769017674960196 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 33/2 → 69/4 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 33/2 → 18/1 | gain:0.6 note:F3 velocity:0.5081270858645439 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ (135/8 → 18/1) ⇝ 147/8 | gain:0.6 note:C4 velocity:0.6415294744074345 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 69/4 → 18/1 | note:G2 s:bass clip:1 gain:0.8 ]", - "[ (69/4 → 18/1) ⇝ 75/4 | gain:0.6 note:C4 velocity:0.6415294744074345 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 135/8 ⇜ (18/1 → 147/8) | gain:0.6 note:G3 velocity:0.583311184309423 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 69/4 ⇜ (18/1 → 75/4) | gain:0.6 note:F3 velocity:0.5062594395130873 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (18/1 → 19/1) ⇝ 39/2 | note:F2 s:bass clip:1 gain:0.8 ]", - "[ (18/1 → 19/1) ⇝ 81/4 | gain:0.6 note:F5 velocity:0.9528779787942767 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (147/8 → 19/1) ⇝ 165/8 | gain:0.6 note:G5 velocity:0.9961825357750058 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (75/4 → 19/1) ⇝ 21/1 | gain:0.6 note:C4 velocity:0.6617662012577057 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 18/1 ⇜ (19/1 → 39/2) | note:F2 s:bass clip:1 gain:0.8 ]", - "[ 18/1 ⇜ (19/1 → 20/1) ⇝ 81/4 | gain:0.6 note:F5 velocity:0.9528779787942767 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 147/8 ⇜ (19/1 → 20/1) ⇝ 165/8 | gain:0.6 note:G5 velocity:0.9961825357750058 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 75/4 ⇜ (19/1 → 20/1) ⇝ 21/1 | gain:0.6 note:C4 velocity:0.6617662012577057 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (39/2 → 20/1) ⇝ 81/4 | note:F2 s:bass clip:1 gain:0.8 ]", - "[ 18/1 ⇜ (20/1 → 81/4) | gain:0.6 note:F5 velocity:0.9528779787942767 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 39/2 ⇜ (20/1 → 81/4) | note:F2 s:bass clip:1 gain:0.8 ]", - "[ 147/8 ⇜ (20/1 → 165/8) | gain:0.6 note:G5 velocity:0.9961825357750058 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 75/4 ⇜ (20/1 → 21/1) | gain:0.6 note:C4 velocity:0.6617662012577057 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ (69/4 → 18/1) ⇝ 75/4 | gain:0.6 note:A3 velocity:0.6086445553228259 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 135/8 ⇜ (18/1 → 147/8) | gain:0.6 note:C4 velocity:0.6415294744074345 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 69/4 ⇜ (18/1 → 75/4) | gain:0.6 note:A3 velocity:0.6086445553228259 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 18/1 → 39/2 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 18/1 → 81/4 | gain:0.6 note:F5 velocity:0.9528779787942767 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 147/8 → 165/8 | gain:0.6 note:G5 velocity:0.9961825357750058 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 75/4 → 21/1 | gain:0.6 note:C4 velocity:0.6617662012577057 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 39/2 → 81/4 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 81/4 → 21/1 | note:F2 s:bass clip:1 gain:0.8 ]", - "[ (81/4 → 21/1) ⇝ 45/2 | gain:0.6 note:D5 velocity:0.9066732861101627 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (165/8 → 21/1) ⇝ 183/8 | gain:0.6 note:G5 velocity:0.9695742893964052 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 81/4 ⇜ (21/1 → 22/1) ⇝ 45/2 | gain:0.6 note:D5 velocity:0.9066732861101627 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 165/8 ⇜ (21/1 → 22/1) ⇝ 183/8 | gain:0.6 note:G5 velocity:0.9695742893964052 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (21/1 → 22/1) ⇝ 45/2 | note:F2 s:bass clip:1 gain:0.8 ]", - "[ (21/1 → 22/1) ⇝ 93/4 | gain:0.6 note:G5 velocity:0.9865687442943454 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 81/4 ⇜ (22/1 → 45/2) | gain:0.6 note:D5 velocity:0.9066732861101627 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 21/1 ⇜ (22/1 → 45/2) | note:F2 s:bass clip:1 gain:0.8 ]", - "[ 165/8 ⇜ (22/1 → 183/8) | gain:0.6 note:G5 velocity:0.9695742893964052 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 21/1 ⇜ (22/1 → 23/1) ⇝ 93/4 | gain:0.6 note:G5 velocity:0.9865687442943454 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (45/2 → 23/1) ⇝ 93/4 | note:F2 s:bass clip:1 gain:0.8 ]", - "[ (45/2 → 23/1) ⇝ 24/1 | gain:0.6 note:C5 velocity:0.8680832693353295 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ (183/8 → 23/1) ⇝ 195/8 | gain:0.6 note:D4 velocity:0.7065566582605243 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 21/1 ⇜ (23/1 → 93/4) | gain:0.6 note:G5 velocity:0.9865687442943454 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 45/2 ⇜ (23/1 → 93/4) | note:F2 s:bass clip:1 gain:0.8 ]", - "[ 45/2 ⇜ (23/1 → 24/1) | gain:0.6 note:C5 velocity:0.8680832693353295 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", - "[ 183/8 ⇜ (23/1 → 24/1) ⇝ 195/8 | gain:0.6 note:D4 velocity:0.7065566582605243 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 81/4 → 45/2 | gain:0.6 note:D5 velocity:0.9066732861101627 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 165/8 → 183/8 | gain:0.6 note:G5 velocity:0.9695742893964052 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 21/1 → 45/2 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 21/1 → 93/4 | gain:0.6 note:G5 velocity:0.9865687442943454 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ 45/2 → 93/4 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 45/2 → 24/1 | gain:0.6 note:C5 velocity:0.8680832693353295 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ (183/8 → 24/1) ⇝ 195/8 | gain:0.6 note:C4 velocity:0.6528211180120707 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", "[ 93/4 → 24/1 | note:F2 s:bass clip:1 gain:0.8 ]", - "[ (93/4 → 24/1) ⇝ 99/4 | gain:0.6 note:C4 velocity:0.6528211180120707 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", + "[ (93/4 → 24/1) ⇝ 99/4 | gain:0.6 note:F3 velocity:0.5404728800058365 s:bell delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]", ] `; @@ -8620,24 +8429,19 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 0/1 → 3/4 | note:Bb4 ]", "[ 0/1 → 3/4 | note:D5 ]", "[ 0/1 → 3/4 | note:G3 ]", - "[ (3/4 → 1/1) ⇝ 5/4 | note:D4 ]", - "[ (3/4 → 1/1) ⇝ 5/4 | note:G4 ]", - "[ (3/4 → 1/1) ⇝ 5/4 | note:Bb4 ]", - "[ (3/4 → 1/1) ⇝ 3/2 | note:G3 ]", - "[ 3/4 ⇜ (1/1 → 5/4) | note:D4 ]", - "[ 3/4 ⇜ (1/1 → 5/4) | note:G4 ]", - "[ 3/4 ⇜ (1/1 → 5/4) | note:Bb4 ]", - "[ 3/4 ⇜ (1/1 → 3/2) | note:G3 ]", + "[ 3/4 → 5/4 | note:D4 ]", + "[ 3/4 → 5/4 | note:G4 ]", + "[ 3/4 → 5/4 | note:Bb4 ]", + "[ 3/4 → 3/2 | note:G3 ]", "[ 5/4 → 3/2 | note:Bb3 ]", "[ 5/4 → 3/2 | note:D4 ]", "[ 5/4 → 3/2 | note:F4 ]", "[ 3/2 → 2/1 | note:G3 ]", "[ 3/2 → 2/1 | note:C4 ]", "[ 3/2 → 2/1 | note:E4 ]", - "[ (3/2 → 2/1) ⇝ 9/4 | note:C3 ]", + "[ 3/2 → 9/4 | note:C3 ]", "[ 2/1 → 17/8 | note:Ab3 ]", "[ 2/1 → 17/8 | note:F4 ]", - "[ 3/2 ⇜ (2/1 → 9/4) | note:C3 ]", "[ 17/8 → 9/4 | note:A3 ]", "[ 17/8 → 9/4 | note:Gb4 ]", "[ 9/4 → 3/1 | note:Bb3 ]", @@ -8651,19 +8455,17 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 7/2 → 15/4 | note:F3 ]", "[ 7/2 → 15/4 | note:A3 ]", "[ 7/2 → 15/4 | note:C3 ]", - "[ (15/4 → 4/1) ⇝ 9/2 | note:D2 ]", + "[ 15/4 → 9/2 | note:D2 ]", "[ 4/1 → 17/4 | note:F3 ]", "[ 4/1 → 17/4 | note:A3 ]", "[ 4/1 → 17/4 | note:C3 ]", - "[ 15/4 ⇜ (4/1 → 9/2) | note:D2 ]", "[ 17/4 → 9/2 | note:F3 ]", "[ 17/4 → 9/2 | note:A3 ]", "[ 17/4 → 9/2 | note:C3 ]", - "[ (9/2 → 5/1) ⇝ 21/4 | note:G2 ]", + "[ 9/2 → 21/4 | note:G2 ]", "[ 19/4 → 5/1 | note:F3 ]", "[ 19/4 → 5/1 | note:Bb3 ]", "[ 19/4 → 5/1 | note:D3 ]", - "[ 9/2 ⇜ (5/1 → 21/4) | note:G2 ]", "[ 5/1 → 21/4 | note:F3 ]", "[ 5/1 → 21/4 | note:Bb3 ]", "[ 5/1 → 21/4 | note:D3 ]", @@ -8681,19 +8483,17 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 13/2 → 27/4 | note:F3 ]", "[ 13/2 → 27/4 | note:A3 ]", "[ 13/2 → 27/4 | note:C3 ]", - "[ (27/4 → 7/1) ⇝ 15/2 | note:D2 ]", + "[ 27/4 → 15/2 | note:D2 ]", "[ 7/1 → 29/4 | note:F3 ]", "[ 7/1 → 29/4 | note:A3 ]", "[ 7/1 → 29/4 | note:C3 ]", - "[ 27/4 ⇜ (7/1 → 15/2) | note:D2 ]", "[ 29/4 → 15/2 | note:F3 ]", "[ 29/4 → 15/2 | note:A3 ]", "[ 29/4 → 15/2 | note:C3 ]", - "[ (15/2 → 8/1) ⇝ 33/4 | note:G2 ]", + "[ 15/2 → 33/4 | note:G2 ]", "[ 31/4 → 8/1 | note:F3 ]", "[ 31/4 → 8/1 | note:Bb3 ]", "[ 31/4 → 8/1 | note:D3 ]", - "[ 15/2 ⇜ (8/1 → 33/4) | note:G2 ]", "[ 8/1 → 33/4 | note:F3 ]", "[ 8/1 → 33/4 | note:Bb3 ]", "[ 8/1 → 33/4 | note:D3 ]", @@ -8712,23 +8512,20 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 19/2 → 39/4 | note:F3 ]", "[ 19/2 → 39/4 | note:A3 ]", "[ 19/2 → 39/4 | note:C3 ]", - "[ (39/4 → 10/1) ⇝ 41/4 | note:F5 ]", - "[ (39/4 → 10/1) ⇝ 21/2 | note:A2 ]", - "[ 39/4 ⇜ (10/1 → 41/4) | note:F5 ]", + "[ 39/4 → 41/4 | note:F5 ]", + "[ 39/4 → 21/2 | note:A2 ]", "[ 10/1 → 41/4 | note:F3 ]", "[ 10/1 → 41/4 | note:A3 ]", "[ 10/1 → 41/4 | note:C3 ]", - "[ 39/4 ⇜ (10/1 → 21/2) | note:A2 ]", "[ 41/4 → 21/2 | note:C5 ]", "[ 41/4 → 21/2 | note:F3 ]", "[ 41/4 → 21/2 | note:A3 ]", "[ 41/4 → 21/2 | note:C3 ]", "[ 21/2 → 11/1 | note:D5 ]", - "[ (21/2 → 11/1) ⇝ 45/4 | note:Bb2 ]", + "[ 21/2 → 45/4 | note:Bb2 ]", "[ 43/4 → 11/1 | note:F3 ]", "[ 43/4 → 11/1 | note:Bb3 ]", "[ 43/4 → 11/1 | note:D3 ]", - "[ 21/2 ⇜ (11/1 → 45/4) | note:Bb2 ]", "[ 11/1 → 45/4 | note:F5 ]", "[ 11/1 → 45/4 | note:F3 ]", "[ 11/1 → 45/4 | note:Bb3 ]", @@ -8750,24 +8547,20 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 25/2 → 51/4 | note:A3 ]", "[ 25/2 → 51/4 | note:C4 ]", "[ 25/2 → 51/4 | note:E4 ]", - "[ (51/4 → 13/1) ⇝ 53/4 | note:F5 ]", - "[ (51/4 → 13/1) ⇝ 27/2 | note:Ab2 ]", - "[ 51/4 ⇜ (13/1 → 53/4) | note:F5 ]", + "[ 51/4 → 53/4 | note:F5 ]", + "[ 51/4 → 27/2 | note:Ab2 ]", "[ 13/1 → 53/4 | note:Ab3 ]", "[ 13/1 → 53/4 | note:C4 ]", "[ 13/1 → 53/4 | note:Eb4 ]", - "[ 51/4 ⇜ (13/1 → 27/2) | note:Ab2 ]", "[ 53/4 → 27/2 | note:C6 ]", "[ 53/4 → 27/2 | note:Ab3 ]", "[ 53/4 → 27/2 | note:C4 ]", "[ 53/4 → 27/2 | note:Eb4 ]", - "[ (27/2 → 14/1) ⇝ 57/4 | note:A5 ]", - "[ (27/2 → 14/1) ⇝ 57/4 | note:G2 ]", + "[ 27/2 → 57/4 | note:A5 ]", + "[ 27/2 → 57/4 | note:G2 ]", "[ 55/4 → 14/1 | note:F3 ]", "[ 55/4 → 14/1 | note:Bb3 ]", "[ 55/4 → 14/1 | note:D3 ]", - "[ 27/2 ⇜ (14/1 → 57/4) | note:A5 ]", - "[ 27/2 ⇜ (14/1 → 57/4) | note:G2 ]", "[ 14/1 → 57/4 | note:F3 ]", "[ 14/1 → 57/4 | note:Bb3 ]", "[ 14/1 → 57/4 | note:D3 ]", @@ -8787,23 +8580,20 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 31/2 → 63/4 | note:F3 ]", "[ 31/2 → 63/4 | note:A3 ]", "[ 31/2 → 63/4 | note:C4 ]", - "[ (63/4 → 16/1) ⇝ 65/4 | note:F5 ]", - "[ (63/4 → 16/1) ⇝ 33/2 | note:A2 ]", - "[ 63/4 ⇜ (16/1 → 65/4) | note:F5 ]", + "[ 63/4 → 65/4 | note:F5 ]", + "[ 63/4 → 33/2 | note:A2 ]", "[ 16/1 → 65/4 | note:F3 ]", "[ 16/1 → 65/4 | note:A3 ]", "[ 16/1 → 65/4 | note:C4 ]", - "[ 63/4 ⇜ (16/1 → 33/2) | note:A2 ]", "[ 65/4 → 33/2 | note:C5 ]", "[ 65/4 → 33/2 | note:F3 ]", "[ 65/4 → 33/2 | note:A3 ]", "[ 65/4 → 33/2 | note:C4 ]", "[ 33/2 → 17/1 | note:D5 ]", - "[ (33/2 → 17/1) ⇝ 69/4 | note:Bb2 ]", + "[ 33/2 → 69/4 | note:Bb2 ]", "[ 67/4 → 17/1 | note:F3 ]", "[ 67/4 → 17/1 | note:Bb3 ]", "[ 67/4 → 17/1 | note:D3 ]", - "[ 33/2 ⇜ (17/1 → 69/4) | note:Bb2 ]", "[ 17/1 → 69/4 | note:F5 ]", "[ 17/1 → 69/4 | note:F3 ]", "[ 17/1 → 69/4 | note:Bb3 ]", @@ -8826,26 +8616,23 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 37/2 → 75/4 | note:Bb3 ]", "[ 37/2 → 75/4 | note:D4 ]", "[ 75/4 → 19/1 | note:Bb5 ]", - "[ (75/4 → 19/1) ⇝ 39/2 | note:C2 ]", + "[ 75/4 → 39/2 | note:C2 ]", "[ 19/1 → 77/4 | note:A5 ]", "[ 19/1 → 77/4 | note:F3 ]", "[ 19/1 → 77/4 | note:Bb3 ]", "[ 19/1 → 77/4 | note:C4 ]", - "[ 75/4 ⇜ (19/1 → 39/2) | note:C2 ]", "[ 77/4 → 39/2 | note:G5 ]", "[ 77/4 → 39/2 | note:F3 ]", "[ 77/4 → 39/2 | note:Bb3 ]", "[ 77/4 → 39/2 | note:C4 ]", - "[ (39/2 → 20/1) ⇝ 81/4 | note:F2 ]", - "[ (39/2 → 20/1) ⇝ 21/1 | note:F5 ]", + "[ 39/2 → 81/4 | note:F2 ]", + "[ 39/2 → 21/1 | note:F5 ]", "[ 79/4 → 20/1 | note:F3 ]", "[ 79/4 → 20/1 | note:A3 ]", "[ 79/4 → 20/1 | note:C4 ]", - "[ 39/2 ⇜ (20/1 → 81/4) | note:F2 ]", "[ 20/1 → 81/4 | note:F3 ]", "[ 20/1 → 81/4 | note:A3 ]", "[ 20/1 → 81/4 | note:C4 ]", - "[ 39/2 ⇜ (20/1 → 21/1) | note:F5 ]", "[ 81/4 → 21/1 | note:F2 ]", "[ 41/2 → 83/4 | note:F3 ]", "[ 41/2 → 83/4 | note:A3 ]", @@ -8861,23 +8648,20 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 43/2 → 87/4 | note:F3 ]", "[ 43/2 → 87/4 | note:A3 ]", "[ 43/2 → 87/4 | note:C3 ]", - "[ (87/4 → 22/1) ⇝ 89/4 | note:F5 ]", - "[ (87/4 → 22/1) ⇝ 45/2 | note:A2 ]", - "[ 87/4 ⇜ (22/1 → 89/4) | note:F5 ]", + "[ 87/4 → 89/4 | note:F5 ]", + "[ 87/4 → 45/2 | note:A2 ]", "[ 22/1 → 89/4 | note:F3 ]", "[ 22/1 → 89/4 | note:A3 ]", "[ 22/1 → 89/4 | note:C3 ]", - "[ 87/4 ⇜ (22/1 → 45/2) | note:A2 ]", "[ 89/4 → 45/2 | note:C5 ]", "[ 89/4 → 45/2 | note:F3 ]", "[ 89/4 → 45/2 | note:A3 ]", "[ 89/4 → 45/2 | note:C3 ]", "[ 45/2 → 23/1 | note:D5 ]", - "[ (45/2 → 23/1) ⇝ 93/4 | note:Bb2 ]", + "[ 45/2 → 93/4 | note:Bb2 ]", "[ 91/4 → 23/1 | note:F3 ]", "[ 91/4 → 23/1 | note:Bb3 ]", "[ 91/4 → 23/1 | note:D3 ]", - "[ 45/2 ⇜ (23/1 → 93/4) | note:Bb2 ]", "[ 23/1 → 93/4 | note:F5 ]", "[ 23/1 → 93/4 | note:F3 ]", "[ 23/1 → 93/4 | note:Bb3 ]", @@ -8899,24 +8683,20 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 49/2 → 99/4 | note:A3 ]", "[ 49/2 → 99/4 | note:C4 ]", "[ 49/2 → 99/4 | note:E4 ]", - "[ (99/4 → 25/1) ⇝ 101/4 | note:F5 ]", - "[ (99/4 → 25/1) ⇝ 51/2 | note:Ab2 ]", - "[ 99/4 ⇜ (25/1 → 101/4) | note:F5 ]", + "[ 99/4 → 101/4 | note:F5 ]", + "[ 99/4 → 51/2 | note:Ab2 ]", "[ 25/1 → 101/4 | note:Ab3 ]", "[ 25/1 → 101/4 | note:C4 ]", "[ 25/1 → 101/4 | note:Eb4 ]", - "[ 99/4 ⇜ (25/1 → 51/2) | note:Ab2 ]", "[ 101/4 → 51/2 | note:C6 ]", "[ 101/4 → 51/2 | note:Ab3 ]", "[ 101/4 → 51/2 | note:C4 ]", "[ 101/4 → 51/2 | note:Eb4 ]", - "[ (51/2 → 26/1) ⇝ 105/4 | note:A5 ]", - "[ (51/2 → 26/1) ⇝ 105/4 | note:G2 ]", + "[ 51/2 → 105/4 | note:A5 ]", + "[ 51/2 → 105/4 | note:G2 ]", "[ 103/4 → 26/1 | note:F3 ]", "[ 103/4 → 26/1 | note:Bb3 ]", "[ 103/4 → 26/1 | note:D3 ]", - "[ 51/2 ⇜ (26/1 → 105/4) | note:A5 ]", - "[ 51/2 ⇜ (26/1 → 105/4) | note:G2 ]", "[ 26/1 → 105/4 | note:F3 ]", "[ 26/1 → 105/4 | note:Bb3 ]", "[ 26/1 → 105/4 | note:D3 ]", @@ -8936,23 +8716,20 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 55/2 → 111/4 | note:F3 ]", "[ 55/2 → 111/4 | note:A3 ]", "[ 55/2 → 111/4 | note:C3 ]", - "[ (111/4 → 28/1) ⇝ 113/4 | note:F5 ]", - "[ (111/4 → 28/1) ⇝ 57/2 | note:A2 ]", - "[ 111/4 ⇜ (28/1 → 113/4) | note:F5 ]", + "[ 111/4 → 113/4 | note:F5 ]", + "[ 111/4 → 57/2 | note:A2 ]", "[ 28/1 → 113/4 | note:F3 ]", "[ 28/1 → 113/4 | note:A3 ]", "[ 28/1 → 113/4 | note:C3 ]", - "[ 111/4 ⇜ (28/1 → 57/2) | note:A2 ]", "[ 113/4 → 57/2 | note:C5 ]", "[ 113/4 → 57/2 | note:F3 ]", "[ 113/4 → 57/2 | note:A3 ]", "[ 113/4 → 57/2 | note:C3 ]", "[ 57/2 → 29/1 | note:D5 ]", - "[ (57/2 → 29/1) ⇝ 117/4 | note:Bb2 ]", + "[ 57/2 → 117/4 | note:Bb2 ]", "[ 115/4 → 29/1 | note:F3 ]", "[ 115/4 → 29/1 | note:Bb3 ]", "[ 115/4 → 29/1 | note:D3 ]", - "[ 57/2 ⇜ (29/1 → 117/4) | note:Bb2 ]", "[ 29/1 → 117/4 | note:F5 ]", "[ 29/1 → 117/4 | note:F3 ]", "[ 29/1 → 117/4 | note:Bb3 ]", @@ -8975,26 +8752,23 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 61/2 → 123/4 | note:Bb3 ]", "[ 61/2 → 123/4 | note:D4 ]", "[ 123/4 → 31/1 | note:Bb5 ]", - "[ (123/4 → 31/1) ⇝ 63/2 | note:C2 ]", + "[ 123/4 → 63/2 | note:C2 ]", "[ 31/1 → 125/4 | note:A5 ]", "[ 31/1 → 125/4 | note:F3 ]", "[ 31/1 → 125/4 | note:Bb3 ]", "[ 31/1 → 125/4 | note:C4 ]", - "[ 123/4 ⇜ (31/1 → 63/2) | note:C2 ]", "[ 125/4 → 63/2 | note:G5 ]", "[ 125/4 → 63/2 | note:F3 ]", "[ 125/4 → 63/2 | note:Bb3 ]", "[ 125/4 → 63/2 | note:C4 ]", - "[ (63/2 → 32/1) ⇝ 129/4 | note:F2 ]", - "[ (63/2 → 32/1) ⇝ 33/1 | note:F5 ]", + "[ 63/2 → 129/4 | note:F2 ]", + "[ 63/2 → 33/1 | note:F5 ]", "[ 127/4 → 32/1 | note:F3 ]", "[ 127/4 → 32/1 | note:A3 ]", "[ 127/4 → 32/1 | note:C4 ]", - "[ 63/2 ⇜ (32/1 → 129/4) | note:F2 ]", "[ 32/1 → 129/4 | note:F3 ]", "[ 32/1 → 129/4 | note:A3 ]", "[ 32/1 → 129/4 | note:C4 ]", - "[ 63/2 ⇜ (32/1 → 33/1) | note:F5 ]", "[ 129/4 → 33/1 | note:F2 ]", "[ 65/2 → 131/4 | note:F3 ]", "[ 65/2 → 131/4 | note:A3 ]", @@ -9010,24 +8784,20 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 67/2 → 135/4 | note:Bb3 ]", "[ 67/2 → 135/4 | note:D3 ]", "[ 67/2 → 135/4 | note:F4 ]", - "[ (135/4 → 34/1) ⇝ 137/4 | note:F5 ]", - "[ (135/4 → 34/1) ⇝ 69/2 | note:Bb2 ]", - "[ 135/4 ⇜ (34/1 → 137/4) | note:F5 ]", + "[ 135/4 → 137/4 | note:F5 ]", + "[ 135/4 → 69/2 | note:Bb2 ]", "[ 34/1 → 137/4 | note:Bb3 ]", "[ 34/1 → 137/4 | note:D3 ]", "[ 34/1 → 137/4 | note:F4 ]", - "[ 135/4 ⇜ (34/1 → 69/2) | note:Bb2 ]", "[ 137/4 → 69/2 | note:C5 ]", "[ 137/4 → 69/2 | note:Bb3 ]", "[ 137/4 → 69/2 | note:D3 ]", "[ 137/4 → 69/2 | note:F4 ]", - "[ (69/2 → 35/1) ⇝ 141/4 | note:A5 ]", - "[ (69/2 → 35/1) ⇝ 141/4 | note:A2 ]", + "[ 69/2 → 141/4 | note:A5 ]", + "[ 69/2 → 141/4 | note:A2 ]", "[ 139/4 → 35/1 | note:A3 ]", "[ 139/4 → 35/1 | note:C4 ]", "[ 139/4 → 35/1 | note:F4 ]", - "[ 69/2 ⇜ (35/1 → 141/4) | note:A5 ]", - "[ 69/2 ⇜ (35/1 → 141/4) | note:A2 ]", "[ 35/1 → 141/4 | note:A3 ]", "[ 35/1 → 141/4 | note:C4 ]", "[ 35/1 → 141/4 | note:F4 ]", @@ -9047,27 +8817,23 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 73/2 → 147/4 | note:Ab3 ]", "[ 73/2 → 147/4 | note:B3 ]", "[ 73/2 → 147/4 | note:F4 ]", - "[ (147/4 → 37/1) ⇝ 149/4 | note:F5 ]", - "[ (147/4 → 37/1) ⇝ 75/2 | note:Ab2 ]", - "[ 147/4 ⇜ (37/1 → 149/4) | note:F5 ]", + "[ 147/4 → 149/4 | note:F5 ]", + "[ 147/4 → 75/2 | note:Ab2 ]", "[ 37/1 → 149/4 | note:Ab3 ]", "[ 37/1 → 149/4 | note:B3 ]", "[ 37/1 → 149/4 | note:F4 ]", - "[ 147/4 ⇜ (37/1 → 75/2) | note:Ab2 ]", "[ 149/4 → 75/2 | note:Ab5 ]", "[ 149/4 → 75/2 | note:Ab3 ]", "[ 149/4 → 75/2 | note:B3 ]", "[ 149/4 → 75/2 | note:F4 ]", - "[ (75/2 → 38/1) ⇝ 153/4 | note:G2 ]", - "[ (75/2 → 38/1) ⇝ 39/1 | note:G5 ]", + "[ 75/2 → 153/4 | note:G2 ]", + "[ 75/2 → 39/1 | note:G5 ]", "[ 151/4 → 38/1 | note:G3 ]", "[ 151/4 → 38/1 | note:Bb3 ]", "[ 151/4 → 38/1 | note:F4 ]", - "[ 75/2 ⇜ (38/1 → 153/4) | note:G2 ]", "[ 38/1 → 153/4 | note:G3 ]", "[ 38/1 → 153/4 | note:Bb3 ]", "[ 38/1 → 153/4 | note:F4 ]", - "[ 75/2 ⇜ (38/1 → 39/1) | note:G5 ]", "[ 153/4 → 77/2 | note:C2 ]", "[ 77/2 → 155/4 | note:G3 ]", "[ 77/2 → 155/4 | note:Bb3 ]", @@ -9085,24 +8851,20 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 79/2 → 159/4 | note:Bb3 ]", "[ 79/2 → 159/4 | note:D3 ]", "[ 79/2 → 159/4 | note:F4 ]", - "[ (159/4 → 40/1) ⇝ 161/4 | note:F5 ]", - "[ (159/4 → 40/1) ⇝ 81/2 | note:Bb2 ]", - "[ 159/4 ⇜ (40/1 → 161/4) | note:F5 ]", + "[ 159/4 → 161/4 | note:F5 ]", + "[ 159/4 → 81/2 | note:Bb2 ]", "[ 40/1 → 161/4 | note:Bb3 ]", "[ 40/1 → 161/4 | note:D3 ]", "[ 40/1 → 161/4 | note:F4 ]", - "[ 159/4 ⇜ (40/1 → 81/2) | note:Bb2 ]", "[ 161/4 → 81/2 | note:C5 ]", "[ 161/4 → 81/2 | note:Bb3 ]", "[ 161/4 → 81/2 | note:D3 ]", "[ 161/4 → 81/2 | note:F4 ]", - "[ (81/2 → 41/1) ⇝ 165/4 | note:A5 ]", - "[ (81/2 → 41/1) ⇝ 165/4 | note:A2 ]", + "[ 81/2 → 165/4 | note:A5 ]", + "[ 81/2 → 165/4 | note:A2 ]", "[ 163/4 → 41/1 | note:A3 ]", "[ 163/4 → 41/1 | note:C4 ]", "[ 163/4 → 41/1 | note:F4 ]", - "[ 81/2 ⇜ (41/1 → 165/4) | note:A5 ]", - "[ 81/2 ⇜ (41/1 → 165/4) | note:A2 ]", "[ 41/1 → 165/4 | note:A3 ]", "[ 41/1 → 165/4 | note:C4 ]", "[ 41/1 → 165/4 | note:F4 ]", @@ -9122,27 +8884,23 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 85/2 → 171/4 | note:Ab3 ]", "[ 85/2 → 171/4 | note:B3 ]", "[ 85/2 → 171/4 | note:F4 ]", - "[ (171/4 → 43/1) ⇝ 173/4 | note:F5 ]", - "[ (171/4 → 43/1) ⇝ 87/2 | note:Ab2 ]", - "[ 171/4 ⇜ (43/1 → 173/4) | note:F5 ]", + "[ 171/4 → 173/4 | note:F5 ]", + "[ 171/4 → 87/2 | note:Ab2 ]", "[ 43/1 → 173/4 | note:Ab3 ]", "[ 43/1 → 173/4 | note:B3 ]", "[ 43/1 → 173/4 | note:F4 ]", - "[ 171/4 ⇜ (43/1 → 87/2) | note:Ab2 ]", "[ 173/4 → 87/2 | note:C5 ]", "[ 173/4 → 87/2 | note:Ab3 ]", "[ 173/4 → 87/2 | note:B3 ]", "[ 173/4 → 87/2 | note:F4 ]", - "[ (87/2 → 44/1) ⇝ 177/4 | note:G2 ]", - "[ (87/2 → 44/1) ⇝ 45/1 | note:C6 ]", + "[ 87/2 → 177/4 | note:G2 ]", + "[ 87/2 → 45/1 | note:C6 ]", "[ 175/4 → 44/1 | note:G3 ]", "[ 175/4 → 44/1 | note:Bb3 ]", "[ 175/4 → 44/1 | note:F4 ]", - "[ 87/2 ⇜ (44/1 → 177/4) | note:G2 ]", "[ 44/1 → 177/4 | note:G3 ]", "[ 44/1 → 177/4 | note:Bb3 ]", "[ 44/1 → 177/4 | note:F4 ]", - "[ 87/2 ⇜ (44/1 → 45/1) | note:C6 ]", "[ 177/4 → 89/2 | note:C2 ]", "[ 89/2 → 179/4 | note:G3 ]", "[ 89/2 → 179/4 | note:Bb3 ]", @@ -9160,23 +8918,20 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 91/2 → 183/4 | note:F3 ]", "[ 91/2 → 183/4 | note:A3 ]", "[ 91/2 → 183/4 | note:C3 ]", - "[ (183/4 → 46/1) ⇝ 185/4 | note:F5 ]", - "[ (183/4 → 46/1) ⇝ 93/2 | note:A2 ]", - "[ 183/4 ⇜ (46/1 → 185/4) | note:F5 ]", + "[ 183/4 → 185/4 | note:F5 ]", + "[ 183/4 → 93/2 | note:A2 ]", "[ 46/1 → 185/4 | note:F3 ]", "[ 46/1 → 185/4 | note:A3 ]", "[ 46/1 → 185/4 | note:C3 ]", - "[ 183/4 ⇜ (46/1 → 93/2) | note:A2 ]", "[ 185/4 → 93/2 | note:C5 ]", "[ 185/4 → 93/2 | note:F3 ]", "[ 185/4 → 93/2 | note:A3 ]", "[ 185/4 → 93/2 | note:C3 ]", "[ 93/2 → 47/1 | note:D5 ]", - "[ (93/2 → 47/1) ⇝ 189/4 | note:Bb2 ]", + "[ 93/2 → 189/4 | note:Bb2 ]", "[ 187/4 → 47/1 | note:F3 ]", "[ 187/4 → 47/1 | note:Bb3 ]", "[ 187/4 → 47/1 | note:D3 ]", - "[ 93/2 ⇜ (47/1 → 189/4) | note:Bb2 ]", "[ 47/1 → 189/4 | note:F5 ]", "[ 47/1 → 189/4 | note:F3 ]", "[ 47/1 → 189/4 | note:Bb3 ]", @@ -9199,26 +8954,23 @@ exports[`renders tunes > tune: swimming 1`] = ` "[ 97/2 → 195/4 | note:Bb3 ]", "[ 97/2 → 195/4 | note:D4 ]", "[ 195/4 → 49/1 | note:Bb5 ]", - "[ (195/4 → 49/1) ⇝ 99/2 | note:C2 ]", + "[ 195/4 → 99/2 | note:C2 ]", "[ 49/1 → 197/4 | note:A5 ]", "[ 49/1 → 197/4 | note:F3 ]", "[ 49/1 → 197/4 | note:Bb3 ]", "[ 49/1 → 197/4 | note:C4 ]", - "[ 195/4 ⇜ (49/1 → 99/2) | note:C2 ]", "[ 197/4 → 99/2 | note:G5 ]", "[ 197/4 → 99/2 | note:F3 ]", "[ 197/4 → 99/2 | note:Bb3 ]", "[ 197/4 → 99/2 | note:C4 ]", - "[ (99/2 → 50/1) ⇝ 201/4 | note:F2 ]", - "[ (99/2 → 50/1) ⇝ 51/1 | note:F5 ]", + "[ 99/2 → 201/4 | note:F2 ]", + "[ 99/2 → 51/1 | note:F5 ]", "[ 199/4 → 50/1 | note:F3 ]", "[ 199/4 → 50/1 | note:A3 ]", "[ 199/4 → 50/1 | note:C4 ]", - "[ 99/2 ⇜ (50/1 → 201/4) | note:F2 ]", "[ 50/1 → 201/4 | note:F3 ]", "[ 50/1 → 201/4 | note:A3 ]", "[ 50/1 → 201/4 | note:C4 ]", - "[ 99/2 ⇜ (50/1 → 51/1) | note:F5 ]", "[ 201/4 → 51/1 | note:F2 ]", "[ 101/2 → 203/4 | note:F3 ]", "[ 101/2 → 203/4 | note:A3 ]", @@ -9240,35 +8992,24 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ (0/1 → 3/8) ⇝ 3/4 | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", "[ -9/16 ⇜ (0/1 → 9/16) ⇝ 15/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", "[ 0/1 → 3/4 | gain:1 note:C3 clip:0.1 ]", - "[ (0/1 → 1/1) ⇝ 3/2 | gain:1 note:G2 clip:0.1 ]", + "[ 0/1 → 3/2 | gain:1 note:G2 clip:0.1 ]", "[ 3/16 → 3/8 | s:bd gain:0.7 ]", "[ 3/16 → 9/16 | gain:1 note:C2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", "[ (3/16 → 9/16) ⇝ 15/16 | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", "[ 3/16 → 15/16 | gain:0.4 note:C4 clip:0.1 ]", - "[ (3/16 → 1/1) ⇝ 27/16 | gain:0.4 note:G3 clip:0.1 ]", + "[ (3/16 → 3/2) ⇝ 27/16 | gain:0.4 note:G3 clip:0.1 ]", "[ 3/8 → 3/4 | s:hh gain:0.7 ]", "[ 3/8 → 3/4 | gain:1 note:A1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (3/8 → 1/1) ⇝ 9/8 | gain:0.16000000000000003 note:C5 clip:0.1 ]", - "[ (3/8 → 1/1) ⇝ 15/8 | gain:0.16000000000000003 note:G4 clip:0.1 ]", + "[ 3/8 → 9/8 | gain:0.16000000000000003 note:C5 clip:0.1 ]", + "[ (3/8 → 3/2) ⇝ 15/8 | gain:0.16000000000000003 note:G4 clip:0.1 ]", "[ 9/16 → 15/16 | gain:1 note:A1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (9/16 → 1/1) ⇝ 21/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ (9/16 → 1/1) ⇝ 33/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ (3/4 → 1/1) ⇝ 9/8 | s:sn gain:0.7 ]", - "[ (3/4 → 1/1) ⇝ 9/8 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (3/4 → 1/1) ⇝ 3/2 | gain:1 note:Eb3 clip:0.1 ]", - "[ (15/16 → 1/1) ⇝ 21/16 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (15/16 → 1/1) ⇝ 27/16 | gain:0.4 note:Eb4 clip:0.1 ]", - "[ 3/8 ⇜ (1/1 → 9/8) | gain:0.16000000000000003 note:C5 clip:0.1 ]", - "[ 3/4 ⇜ (1/1 → 9/8) | s:sn gain:0.7 ]", - "[ 3/4 ⇜ (1/1 → 9/8) | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 9/16 ⇜ (1/1 → 21/16) | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ 15/16 ⇜ (1/1 → 21/16) | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 0/1 ⇜ (1/1 → 3/2) | gain:1 note:G2 clip:0.1 ]", - "[ 3/16 ⇜ (1/1 → 3/2) ⇝ 27/16 | gain:0.4 note:G3 clip:0.1 ]", - "[ 3/8 ⇜ (1/1 → 3/2) ⇝ 15/8 | gain:0.16000000000000003 note:G4 clip:0.1 ]", - "[ 9/16 ⇜ (1/1 → 3/2) ⇝ 33/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ 3/4 ⇜ (1/1 → 3/2) | gain:1 note:Eb3 clip:0.1 ]", - "[ 15/16 ⇜ (1/1 → 3/2) ⇝ 27/16 | gain:0.4 note:Eb4 clip:0.1 ]", + "[ 9/16 → 21/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", + "[ (9/16 → 3/2) ⇝ 33/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", + "[ 3/4 → 9/8 | s:sn gain:0.7 ]", + "[ 3/4 → 9/8 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ 3/4 → 3/2 | gain:1 note:Eb3 clip:0.1 ]", + "[ 15/16 → 21/16 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ (15/16 → 3/2) ⇝ 27/16 | gain:0.4 note:Eb4 clip:0.1 ]", "[ 9/8 → 3/2 | s:hh gain:0.7 ]", "[ (9/8 → 3/2) ⇝ 15/8 | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", "[ (21/16 → 3/2) ⇝ 33/16 | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", @@ -9278,24 +9019,15 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ 9/8 ⇜ (3/2 → 15/8) | gain:1 note:C3 clip:0.1 ]", "[ 9/8 ⇜ (3/2 → 15/8) | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", "[ 3/2 → 15/8 | s:bd gain:0.7 ]", - "[ 9/16 ⇜ (3/2 → 2/1) ⇝ 33/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ 9/8 ⇜ (3/2 → 2/1) ⇝ 21/8 | gain:1 note:G2 clip:0.1 ]", - "[ 21/16 ⇜ (3/2 → 2/1) ⇝ 33/16 | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", - "[ 21/16 ⇜ (27/16 → 2/1) ⇝ 33/16 | gain:0.4 note:C4 clip:0.1 ]", - "[ 21/16 ⇜ (27/16 → 2/1) ⇝ 45/16 | gain:0.4 note:G3 clip:0.1 ]", - "[ 3/2 ⇜ (15/8 → 2/1) ⇝ 9/4 | gain:0.16000000000000003 note:C5 clip:0.1 ]", - "[ 3/2 ⇜ (15/8 → 2/1) ⇝ 3/1 | gain:0.16000000000000003 note:G4 clip:0.1 ]", - "[ (15/8 → 2/1) ⇝ 9/4 | s:hh gain:0.7 ]", - "[ (15/8 → 2/1) ⇝ 21/8 | gain:1 note:Eb3 clip:0.1 ]", - "[ 9/16 ⇜ (2/1 → 33/16) | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ 21/16 ⇜ (2/1 → 33/16) | gain:0.4 note:C4 clip:0.1 ]", - "[ 21/16 ⇜ (2/1 → 33/16) | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", - "[ 3/2 ⇜ (2/1 → 9/4) | gain:0.16000000000000003 note:C5 clip:0.1 ]", - "[ 15/8 ⇜ (2/1 → 9/4) | s:hh gain:0.7 ]", - "[ 9/8 ⇜ (2/1 → 21/8) | gain:1 note:G2 clip:0.1 ]", - "[ 15/8 ⇜ (2/1 → 21/8) | gain:1 note:Eb3 clip:0.1 ]", - "[ 21/16 ⇜ (2/1 → 45/16) | gain:0.4 note:G3 clip:0.1 ]", - "[ 3/2 ⇜ (2/1 → 3/1) | gain:0.16000000000000003 note:G4 clip:0.1 ]", + "[ 9/16 ⇜ (3/2 → 33/16) | gain:0.06400000000000002 note:G5 clip:0.1 ]", + "[ 21/16 ⇜ (3/2 → 33/16) | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", + "[ 9/8 ⇜ (3/2 → 21/8) | gain:1 note:G2 clip:0.1 ]", + "[ 21/16 ⇜ (27/16 → 33/16) | gain:0.4 note:C4 clip:0.1 ]", + "[ 21/16 ⇜ (27/16 → 45/16) | gain:0.4 note:G3 clip:0.1 ]", + "[ 3/2 ⇜ (15/8 → 9/4) | gain:0.16000000000000003 note:C5 clip:0.1 ]", + "[ 15/8 → 9/4 | s:hh gain:0.7 ]", + "[ 15/8 → 21/8 | gain:1 note:Eb3 clip:0.1 ]", + "[ 3/2 ⇜ (15/8 → 3/1) | gain:0.16000000000000003 note:G4 clip:0.1 ]", "[ 27/16 ⇜ (33/16 → 39/16) | gain:0.06400000000000002 note:C6 clip:0.1 ]", "[ 33/16 → 45/16 | gain:0.4 note:Eb4 clip:0.1 ]", "[ 27/16 ⇜ (33/16 → 3/1) ⇝ 51/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", @@ -9325,29 +9057,18 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ 51/16 → 63/16 | gain:0.4 note:Eb4 clip:0.1 ]", "[ 27/8 → 15/4 | s:hh gain:0.7 ]", "[ 27/8 → 15/4 | gain:1 note:A1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 21/8 ⇜ (27/8 → 4/1) ⇝ 33/8 | gain:0.16000000000000003 note:G4 clip:0.1 ]", - "[ (27/8 → 4/1) ⇝ 33/8 | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", + "[ 21/8 ⇜ (27/8 → 33/8) | gain:0.16000000000000003 note:G4 clip:0.1 ]", + "[ 27/8 → 33/8 | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", "[ 57/16 → 63/16 | gain:1 note:A1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 45/16 ⇜ (57/16 → 4/1) ⇝ 69/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ (57/16 → 4/1) ⇝ 69/16 | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", - "[ (15/4 → 4/1) ⇝ 33/8 | s:sn gain:0.7 ]", - "[ (15/4 → 4/1) ⇝ 33/8 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (15/4 → 4/1) ⇝ 9/2 | gain:1 note:C3 clip:0.1 ]", - "[ (15/4 → 4/1) ⇝ 21/4 | gain:1 note:G2 clip:0.1 ]", - "[ (63/16 → 4/1) ⇝ 69/16 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (63/16 → 4/1) ⇝ 75/16 | gain:0.4 note:C4 clip:0.1 ]", - "[ (63/16 → 4/1) ⇝ 87/16 | gain:0.4 note:G3 clip:0.1 ]", - "[ 21/8 ⇜ (4/1 → 33/8) | gain:0.16000000000000003 note:G4 clip:0.1 ]", - "[ 27/8 ⇜ (4/1 → 33/8) | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", - "[ 15/4 ⇜ (4/1 → 33/8) | s:sn gain:0.7 ]", - "[ 15/4 ⇜ (4/1 → 33/8) | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 45/16 ⇜ (4/1 → 69/16) | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ 57/16 ⇜ (4/1 → 69/16) | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", - "[ 63/16 ⇜ (4/1 → 69/16) | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 15/4 ⇜ (4/1 → 9/2) | gain:1 note:C3 clip:0.1 ]", - "[ 15/4 ⇜ (4/1 → 9/2) ⇝ 21/4 | gain:1 note:G2 clip:0.1 ]", - "[ 63/16 ⇜ (4/1 → 9/2) ⇝ 75/16 | gain:0.4 note:C4 clip:0.1 ]", - "[ 63/16 ⇜ (4/1 → 9/2) ⇝ 87/16 | gain:0.4 note:G3 clip:0.1 ]", + "[ 45/16 ⇜ (57/16 → 69/16) | gain:0.06400000000000002 note:G5 clip:0.1 ]", + "[ 57/16 → 69/16 | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", + "[ 15/4 → 33/8 | s:sn gain:0.7 ]", + "[ 15/4 → 33/8 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ 15/4 → 9/2 | gain:1 note:C3 clip:0.1 ]", + "[ (15/4 → 9/2) ⇝ 21/4 | gain:1 note:G2 clip:0.1 ]", + "[ 63/16 → 69/16 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ (63/16 → 9/2) ⇝ 75/16 | gain:0.4 note:C4 clip:0.1 ]", + "[ (63/16 → 9/2) ⇝ 87/16 | gain:0.4 note:G3 clip:0.1 ]", "[ 33/8 → 9/2 | s:hh gain:0.7 ]", "[ (33/8 → 9/2) ⇝ 39/8 | gain:0.16000000000000003 note:C5 clip:0.1 ]", "[ (33/8 → 9/2) ⇝ 45/8 | gain:0.16000000000000003 note:G4 clip:0.1 ]", @@ -9360,24 +9081,15 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ 33/8 ⇜ (9/2 → 39/8) | gain:0.16000000000000003 note:C5 clip:0.1 ]", "[ 33/8 ⇜ (9/2 → 39/8) ⇝ 45/8 | gain:0.16000000000000003 note:G4 clip:0.1 ]", "[ 9/2 → 39/8 | s:bd gain:0.7 ]", - "[ 69/16 ⇜ (9/2 → 5/1) ⇝ 81/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ 69/16 ⇜ (9/2 → 5/1) ⇝ 93/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ 57/16 ⇜ (75/16 → 5/1) ⇝ 81/16 | gain:0.4 note:G3 clip:0.1 ]", - "[ 69/16 ⇜ (75/16 → 5/1) ⇝ 81/16 | gain:0.4 note:Eb4 clip:0.1 ]", - "[ 15/4 ⇜ (39/8 → 5/1) ⇝ 21/4 | gain:0.16000000000000003 note:G4 clip:0.1 ]", - "[ 9/2 ⇜ (39/8 → 5/1) ⇝ 21/4 | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", - "[ (39/8 → 5/1) ⇝ 21/4 | s:hh gain:0.7 ]", - "[ (39/8 → 5/1) ⇝ 45/8 | gain:1 note:C3 clip:0.1 ]", - "[ (39/8 → 5/1) ⇝ 51/8 | gain:1 note:G2 clip:0.1 ]", - "[ 57/16 ⇜ (5/1 → 81/16) | gain:0.4 note:G3 clip:0.1 ]", - "[ 69/16 ⇜ (5/1 → 81/16) | gain:0.4 note:Eb4 clip:0.1 ]", - "[ 69/16 ⇜ (5/1 → 81/16) | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ 69/16 ⇜ (5/1 → 81/16) ⇝ 93/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ 15/4 ⇜ (5/1 → 21/4) | gain:0.16000000000000003 note:G4 clip:0.1 ]", - "[ 9/2 ⇜ (5/1 → 21/4) | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", - "[ 39/8 ⇜ (5/1 → 21/4) | s:hh gain:0.7 ]", - "[ 39/8 ⇜ (5/1 → 45/8) | gain:1 note:C3 clip:0.1 ]", - "[ 39/8 ⇜ (5/1 → 6/1) ⇝ 51/8 | gain:1 note:G2 clip:0.1 ]", + "[ 69/16 ⇜ (9/2 → 81/16) | gain:0.06400000000000002 note:C6 clip:0.1 ]", + "[ 69/16 ⇜ (9/2 → 81/16) ⇝ 93/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", + "[ 57/16 ⇜ (75/16 → 81/16) | gain:0.4 note:G3 clip:0.1 ]", + "[ 69/16 ⇜ (75/16 → 81/16) | gain:0.4 note:Eb4 clip:0.1 ]", + "[ 15/4 ⇜ (39/8 → 21/4) | gain:0.16000000000000003 note:G4 clip:0.1 ]", + "[ 9/2 ⇜ (39/8 → 21/4) | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", + "[ 39/8 → 21/4 | s:hh gain:0.7 ]", + "[ 39/8 → 45/8 | gain:1 note:C3 clip:0.1 ]", + "[ (39/8 → 6/1) ⇝ 51/8 | gain:1 note:G2 clip:0.1 ]", "[ 63/16 ⇜ (81/16 → 87/16) | gain:0.06400000000000002 note:G5 clip:0.1 ]", "[ 75/16 ⇜ (81/16 → 87/16) | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", "[ 81/16 → 93/16 | gain:0.4 note:C4 clip:0.1 ]", @@ -9399,35 +9111,24 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ (6/1 → 51/8) ⇝ 27/4 | gain:0.16000000000000003 note:Ab5 clip:0.1 ]", "[ 87/16 ⇜ (6/1 → 105/16) ⇝ 111/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", "[ 6/1 → 27/4 | gain:1 note:F3 clip:0.1 ]", - "[ (6/1 → 7/1) ⇝ 15/2 | gain:1 note:C3 clip:0.1 ]", + "[ 6/1 → 15/2 | gain:1 note:C3 clip:0.1 ]", "[ 99/16 → 51/8 | s:bd gain:0.7 ]", "[ 99/16 → 105/16 | gain:1 note:F2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", "[ (99/16 → 105/16) ⇝ 111/16 | gain:0.06400000000000002 note:Ab6 clip:0.1 ]", "[ 99/16 → 111/16 | gain:0.4 note:F4 clip:0.1 ]", - "[ (99/16 → 7/1) ⇝ 123/16 | gain:0.4 note:C4 clip:0.1 ]", + "[ (99/16 → 15/2) ⇝ 123/16 | gain:0.4 note:C4 clip:0.1 ]", "[ 51/8 → 27/4 | s:hh gain:0.7 ]", "[ 51/8 → 27/4 | gain:1 note:D2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (51/8 → 7/1) ⇝ 57/8 | gain:0.16000000000000003 note:F5 clip:0.1 ]", - "[ (51/8 → 7/1) ⇝ 63/8 | gain:0.16000000000000003 note:C5 clip:0.1 ]", + "[ 51/8 → 57/8 | gain:0.16000000000000003 note:F5 clip:0.1 ]", + "[ (51/8 → 15/2) ⇝ 63/8 | gain:0.16000000000000003 note:C5 clip:0.1 ]", "[ 105/16 → 111/16 | gain:1 note:D2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (105/16 → 7/1) ⇝ 117/16 | gain:0.06400000000000002 note:F6 clip:0.1 ]", - "[ (105/16 → 7/1) ⇝ 129/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ (27/4 → 7/1) ⇝ 57/8 | s:sn gain:0.7 ]", - "[ (27/4 → 7/1) ⇝ 57/8 | gain:1 note:Eb2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (27/4 → 7/1) ⇝ 15/2 | gain:1 note:Ab3 clip:0.1 ]", - "[ (111/16 → 7/1) ⇝ 117/16 | gain:1 note:Eb2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (111/16 → 7/1) ⇝ 123/16 | gain:0.4 note:Ab4 clip:0.1 ]", - "[ 51/8 ⇜ (7/1 → 57/8) | gain:0.16000000000000003 note:F5 clip:0.1 ]", - "[ 27/4 ⇜ (7/1 → 57/8) | s:sn gain:0.7 ]", - "[ 27/4 ⇜ (7/1 → 57/8) | gain:1 note:Eb2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 105/16 ⇜ (7/1 → 117/16) | gain:0.06400000000000002 note:F6 clip:0.1 ]", - "[ 111/16 ⇜ (7/1 → 117/16) | gain:1 note:Eb2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 6/1 ⇜ (7/1 → 15/2) | gain:1 note:C3 clip:0.1 ]", - "[ 99/16 ⇜ (7/1 → 15/2) ⇝ 123/16 | gain:0.4 note:C4 clip:0.1 ]", - "[ 51/8 ⇜ (7/1 → 15/2) ⇝ 63/8 | gain:0.16000000000000003 note:C5 clip:0.1 ]", - "[ 105/16 ⇜ (7/1 → 15/2) ⇝ 129/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ 27/4 ⇜ (7/1 → 15/2) | gain:1 note:Ab3 clip:0.1 ]", - "[ 111/16 ⇜ (7/1 → 15/2) ⇝ 123/16 | gain:0.4 note:Ab4 clip:0.1 ]", + "[ 105/16 → 117/16 | gain:0.06400000000000002 note:F6 clip:0.1 ]", + "[ (105/16 → 15/2) ⇝ 129/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", + "[ 27/4 → 57/8 | s:sn gain:0.7 ]", + "[ 27/4 → 57/8 | gain:1 note:Eb2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ 27/4 → 15/2 | gain:1 note:Ab3 clip:0.1 ]", + "[ 111/16 → 117/16 | gain:1 note:Eb2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ (111/16 → 15/2) ⇝ 123/16 | gain:0.4 note:Ab4 clip:0.1 ]", "[ 57/8 → 15/2 | s:hh gain:0.7 ]", "[ (57/8 → 15/2) ⇝ 63/8 | gain:0.16000000000000003 note:Ab5 clip:0.1 ]", "[ (117/16 → 15/2) ⇝ 129/16 | gain:0.06400000000000002 note:Ab6 clip:0.1 ]", @@ -9437,24 +9138,15 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ 57/8 ⇜ (15/2 → 63/8) | gain:1 note:F3 clip:0.1 ]", "[ 57/8 ⇜ (15/2 → 63/8) | gain:0.16000000000000003 note:Ab5 clip:0.1 ]", "[ 15/2 → 63/8 | s:bd gain:0.7 ]", - "[ 105/16 ⇜ (15/2 → 8/1) ⇝ 129/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ 57/8 ⇜ (15/2 → 8/1) ⇝ 69/8 | gain:1 note:C3 clip:0.1 ]", - "[ 117/16 ⇜ (15/2 → 8/1) ⇝ 129/16 | gain:0.06400000000000002 note:Ab6 clip:0.1 ]", - "[ 117/16 ⇜ (123/16 → 8/1) ⇝ 129/16 | gain:0.4 note:F4 clip:0.1 ]", - "[ 117/16 ⇜ (123/16 → 8/1) ⇝ 141/16 | gain:0.4 note:C4 clip:0.1 ]", - "[ 15/2 ⇜ (63/8 → 8/1) ⇝ 33/4 | gain:0.16000000000000003 note:F5 clip:0.1 ]", - "[ 15/2 ⇜ (63/8 → 8/1) ⇝ 9/1 | gain:0.16000000000000003 note:C5 clip:0.1 ]", - "[ (63/8 → 8/1) ⇝ 33/4 | s:hh gain:0.7 ]", - "[ (63/8 → 8/1) ⇝ 69/8 | gain:1 note:Ab3 clip:0.1 ]", - "[ 105/16 ⇜ (8/1 → 129/16) | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ 117/16 ⇜ (8/1 → 129/16) | gain:0.4 note:F4 clip:0.1 ]", - "[ 117/16 ⇜ (8/1 → 129/16) | gain:0.06400000000000002 note:Ab6 clip:0.1 ]", - "[ 15/2 ⇜ (8/1 → 33/4) | gain:0.16000000000000003 note:F5 clip:0.1 ]", - "[ 63/8 ⇜ (8/1 → 33/4) | s:hh gain:0.7 ]", - "[ 57/8 ⇜ (8/1 → 69/8) | gain:1 note:C3 clip:0.1 ]", - "[ 63/8 ⇜ (8/1 → 69/8) | gain:1 note:Ab3 clip:0.1 ]", - "[ 117/16 ⇜ (8/1 → 141/16) | gain:0.4 note:C4 clip:0.1 ]", - "[ 15/2 ⇜ (8/1 → 9/1) | gain:0.16000000000000003 note:C5 clip:0.1 ]", + "[ 105/16 ⇜ (15/2 → 129/16) | gain:0.06400000000000002 note:C6 clip:0.1 ]", + "[ 117/16 ⇜ (15/2 → 129/16) | gain:0.06400000000000002 note:Ab6 clip:0.1 ]", + "[ 57/8 ⇜ (15/2 → 69/8) | gain:1 note:C3 clip:0.1 ]", + "[ 117/16 ⇜ (123/16 → 129/16) | gain:0.4 note:F4 clip:0.1 ]", + "[ 117/16 ⇜ (123/16 → 141/16) | gain:0.4 note:C4 clip:0.1 ]", + "[ 15/2 ⇜ (63/8 → 33/4) | gain:0.16000000000000003 note:F5 clip:0.1 ]", + "[ 63/8 → 33/4 | s:hh gain:0.7 ]", + "[ 63/8 → 69/8 | gain:1 note:Ab3 clip:0.1 ]", + "[ 15/2 ⇜ (63/8 → 9/1) | gain:0.16000000000000003 note:C5 clip:0.1 ]", "[ 123/16 ⇜ (129/16 → 135/16) | gain:0.06400000000000002 note:F6 clip:0.1 ]", "[ 129/16 → 141/16 | gain:0.4 note:Ab4 clip:0.1 ]", "[ 123/16 ⇜ (129/16 → 9/1) ⇝ 147/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", @@ -9484,29 +9176,18 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ 147/16 → 159/16 | gain:0.4 note:Eb4 clip:0.1 ]", "[ 75/8 → 39/4 | s:hh gain:0.7 ]", "[ 75/8 → 39/4 | gain:1 note:A1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 69/8 ⇜ (75/8 → 10/1) ⇝ 81/8 | gain:0.16000000000000003 note:G4 clip:0.1 ]", - "[ (75/8 → 10/1) ⇝ 81/8 | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", + "[ 69/8 ⇜ (75/8 → 81/8) | gain:0.16000000000000003 note:G4 clip:0.1 ]", + "[ 75/8 → 81/8 | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", "[ 153/16 → 159/16 | gain:1 note:A1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 141/16 ⇜ (153/16 → 10/1) ⇝ 165/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ (153/16 → 10/1) ⇝ 165/16 | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", - "[ (39/4 → 10/1) ⇝ 81/8 | s:sn gain:0.7 ]", - "[ (39/4 → 10/1) ⇝ 81/8 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (39/4 → 10/1) ⇝ 21/2 | gain:1 note:C3 clip:0.1 ]", - "[ (39/4 → 10/1) ⇝ 45/4 | gain:1 note:G2 clip:0.1 ]", - "[ (159/16 → 10/1) ⇝ 165/16 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (159/16 → 10/1) ⇝ 171/16 | gain:0.4 note:C4 clip:0.1 ]", - "[ (159/16 → 10/1) ⇝ 183/16 | gain:0.4 note:G3 clip:0.1 ]", - "[ 69/8 ⇜ (10/1 → 81/8) | gain:0.16000000000000003 note:G4 clip:0.1 ]", - "[ 75/8 ⇜ (10/1 → 81/8) | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", - "[ 39/4 ⇜ (10/1 → 81/8) | s:sn gain:0.7 ]", - "[ 39/4 ⇜ (10/1 → 81/8) | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 141/16 ⇜ (10/1 → 165/16) | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ 153/16 ⇜ (10/1 → 165/16) | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", - "[ 159/16 ⇜ (10/1 → 165/16) | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 39/4 ⇜ (10/1 → 21/2) | gain:1 note:C3 clip:0.1 ]", - "[ 39/4 ⇜ (10/1 → 21/2) ⇝ 45/4 | gain:1 note:G2 clip:0.1 ]", - "[ 159/16 ⇜ (10/1 → 21/2) ⇝ 171/16 | gain:0.4 note:C4 clip:0.1 ]", - "[ 159/16 ⇜ (10/1 → 21/2) ⇝ 183/16 | gain:0.4 note:G3 clip:0.1 ]", + "[ 141/16 ⇜ (153/16 → 165/16) | gain:0.06400000000000002 note:G5 clip:0.1 ]", + "[ 153/16 → 165/16 | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", + "[ 39/4 → 81/8 | s:sn gain:0.7 ]", + "[ 39/4 → 81/8 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ 39/4 → 21/2 | gain:1 note:C3 clip:0.1 ]", + "[ (39/4 → 21/2) ⇝ 45/4 | gain:1 note:G2 clip:0.1 ]", + "[ 159/16 → 165/16 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ (159/16 → 21/2) ⇝ 171/16 | gain:0.4 note:C4 clip:0.1 ]", + "[ (159/16 → 21/2) ⇝ 183/16 | gain:0.4 note:G3 clip:0.1 ]", "[ 81/8 → 21/2 | s:hh gain:0.7 ]", "[ (81/8 → 21/2) ⇝ 87/8 | gain:0.16000000000000003 note:C5 clip:0.1 ]", "[ (81/8 → 21/2) ⇝ 93/8 | gain:0.16000000000000003 note:G4 clip:0.1 ]", @@ -9519,24 +9200,15 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ 81/8 ⇜ (21/2 → 87/8) | gain:0.16000000000000003 note:C5 clip:0.1 ]", "[ 81/8 ⇜ (21/2 → 87/8) ⇝ 93/8 | gain:0.16000000000000003 note:G4 clip:0.1 ]", "[ 21/2 → 87/8 | s:bd gain:0.7 ]", - "[ 165/16 ⇜ (21/2 → 11/1) ⇝ 177/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ 165/16 ⇜ (21/2 → 11/1) ⇝ 189/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ 153/16 ⇜ (171/16 → 11/1) ⇝ 177/16 | gain:0.4 note:G3 clip:0.1 ]", - "[ 165/16 ⇜ (171/16 → 11/1) ⇝ 177/16 | gain:0.4 note:Eb4 clip:0.1 ]", - "[ 39/4 ⇜ (87/8 → 11/1) ⇝ 45/4 | gain:0.16000000000000003 note:G4 clip:0.1 ]", - "[ 21/2 ⇜ (87/8 → 11/1) ⇝ 45/4 | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", - "[ (87/8 → 11/1) ⇝ 45/4 | s:hh gain:0.7 ]", - "[ (87/8 → 11/1) ⇝ 93/8 | gain:1 note:C3 clip:0.1 ]", - "[ (87/8 → 11/1) ⇝ 99/8 | gain:1 note:G2 clip:0.1 ]", - "[ 153/16 ⇜ (11/1 → 177/16) | gain:0.4 note:G3 clip:0.1 ]", - "[ 165/16 ⇜ (11/1 → 177/16) | gain:0.4 note:Eb4 clip:0.1 ]", - "[ 165/16 ⇜ (11/1 → 177/16) | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ 165/16 ⇜ (11/1 → 177/16) ⇝ 189/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ 39/4 ⇜ (11/1 → 45/4) | gain:0.16000000000000003 note:G4 clip:0.1 ]", - "[ 21/2 ⇜ (11/1 → 45/4) | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", - "[ 87/8 ⇜ (11/1 → 45/4) | s:hh gain:0.7 ]", - "[ 87/8 ⇜ (11/1 → 93/8) | gain:1 note:C3 clip:0.1 ]", - "[ 87/8 ⇜ (11/1 → 12/1) ⇝ 99/8 | gain:1 note:G2 clip:0.1 ]", + "[ 165/16 ⇜ (21/2 → 177/16) | gain:0.06400000000000002 note:C6 clip:0.1 ]", + "[ 165/16 ⇜ (21/2 → 177/16) ⇝ 189/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", + "[ 153/16 ⇜ (171/16 → 177/16) | gain:0.4 note:G3 clip:0.1 ]", + "[ 165/16 ⇜ (171/16 → 177/16) | gain:0.4 note:Eb4 clip:0.1 ]", + "[ 39/4 ⇜ (87/8 → 45/4) | gain:0.16000000000000003 note:G4 clip:0.1 ]", + "[ 21/2 ⇜ (87/8 → 45/4) | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", + "[ 87/8 → 45/4 | s:hh gain:0.7 ]", + "[ 87/8 → 93/8 | gain:1 note:C3 clip:0.1 ]", + "[ (87/8 → 12/1) ⇝ 99/8 | gain:1 note:G2 clip:0.1 ]", "[ 159/16 ⇜ (177/16 → 183/16) | gain:0.06400000000000002 note:G5 clip:0.1 ]", "[ 171/16 ⇜ (177/16 → 183/16) | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", "[ 177/16 → 189/16 | gain:0.4 note:C4 clip:0.1 ]", @@ -9558,35 +9230,24 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ (12/1 → 99/8) ⇝ 51/4 | gain:0.16000000000000003 note:Bb5 clip:0.1 ]", "[ 183/16 ⇜ (12/1 → 201/16) ⇝ 207/16 | gain:0.06400000000000002 note:D6 clip:0.1 ]", "[ 12/1 → 51/4 | gain:1 note:G3 clip:0.1 ]", - "[ (12/1 → 13/1) ⇝ 27/2 | gain:1 note:D3 clip:0.1 ]", + "[ 12/1 → 27/2 | gain:1 note:D3 clip:0.1 ]", "[ 195/16 → 99/8 | s:bd gain:0.7 ]", "[ 195/16 → 201/16 | gain:1 note:G2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", "[ (195/16 → 201/16) ⇝ 207/16 | gain:0.06400000000000002 note:Bb6 clip:0.1 ]", "[ 195/16 → 207/16 | gain:0.4 note:G4 clip:0.1 ]", - "[ (195/16 → 13/1) ⇝ 219/16 | gain:0.4 note:D4 clip:0.1 ]", + "[ (195/16 → 27/2) ⇝ 219/16 | gain:0.4 note:D4 clip:0.1 ]", "[ 99/8 → 51/4 | s:hh gain:0.7 ]", "[ 99/8 → 51/4 | gain:1 note:E2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (99/8 → 13/1) ⇝ 105/8 | gain:0.16000000000000003 note:G5 clip:0.1 ]", - "[ (99/8 → 13/1) ⇝ 111/8 | gain:0.16000000000000003 note:D5 clip:0.1 ]", + "[ 99/8 → 105/8 | gain:0.16000000000000003 note:G5 clip:0.1 ]", + "[ (99/8 → 27/2) ⇝ 111/8 | gain:0.16000000000000003 note:D5 clip:0.1 ]", "[ 201/16 → 207/16 | gain:1 note:E2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (201/16 → 13/1) ⇝ 213/16 | gain:0.06400000000000002 note:G6 clip:0.1 ]", - "[ (201/16 → 13/1) ⇝ 225/16 | gain:0.06400000000000002 note:D6 clip:0.1 ]", - "[ (51/4 → 13/1) ⇝ 105/8 | s:sn gain:0.7 ]", - "[ (51/4 → 13/1) ⇝ 105/8 | gain:1 note:F2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (51/4 → 13/1) ⇝ 27/2 | gain:1 note:Bb3 clip:0.1 ]", - "[ (207/16 → 13/1) ⇝ 213/16 | gain:1 note:F2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (207/16 → 13/1) ⇝ 219/16 | gain:0.4 note:Bb4 clip:0.1 ]", - "[ 99/8 ⇜ (13/1 → 105/8) | gain:0.16000000000000003 note:G5 clip:0.1 ]", - "[ 51/4 ⇜ (13/1 → 105/8) | s:sn gain:0.7 ]", - "[ 51/4 ⇜ (13/1 → 105/8) | gain:1 note:F2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 201/16 ⇜ (13/1 → 213/16) | gain:0.06400000000000002 note:G6 clip:0.1 ]", - "[ 207/16 ⇜ (13/1 → 213/16) | gain:1 note:F2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 12/1 ⇜ (13/1 → 27/2) | gain:1 note:D3 clip:0.1 ]", - "[ 195/16 ⇜ (13/1 → 27/2) ⇝ 219/16 | gain:0.4 note:D4 clip:0.1 ]", - "[ 99/8 ⇜ (13/1 → 27/2) ⇝ 111/8 | gain:0.16000000000000003 note:D5 clip:0.1 ]", - "[ 201/16 ⇜ (13/1 → 27/2) ⇝ 225/16 | gain:0.06400000000000002 note:D6 clip:0.1 ]", - "[ 51/4 ⇜ (13/1 → 27/2) | gain:1 note:Bb3 clip:0.1 ]", - "[ 207/16 ⇜ (13/1 → 27/2) ⇝ 219/16 | gain:0.4 note:Bb4 clip:0.1 ]", + "[ 201/16 → 213/16 | gain:0.06400000000000002 note:G6 clip:0.1 ]", + "[ (201/16 → 27/2) ⇝ 225/16 | gain:0.06400000000000002 note:D6 clip:0.1 ]", + "[ 51/4 → 105/8 | s:sn gain:0.7 ]", + "[ 51/4 → 105/8 | gain:1 note:F2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ 51/4 → 27/2 | gain:1 note:Bb3 clip:0.1 ]", + "[ 207/16 → 213/16 | gain:1 note:F2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ (207/16 → 27/2) ⇝ 219/16 | gain:0.4 note:Bb4 clip:0.1 ]", "[ 105/8 → 27/2 | s:hh gain:0.7 ]", "[ (105/8 → 27/2) ⇝ 111/8 | gain:0.16000000000000003 note:Bb5 clip:0.1 ]", "[ (213/16 → 27/2) ⇝ 225/16 | gain:0.06400000000000002 note:Bb6 clip:0.1 ]", @@ -9596,24 +9257,15 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ 105/8 ⇜ (27/2 → 111/8) | gain:1 note:G3 clip:0.1 ]", "[ 105/8 ⇜ (27/2 → 111/8) | gain:0.16000000000000003 note:Bb5 clip:0.1 ]", "[ 27/2 → 111/8 | s:bd gain:0.7 ]", - "[ 201/16 ⇜ (27/2 → 14/1) ⇝ 225/16 | gain:0.06400000000000002 note:D6 clip:0.1 ]", - "[ 105/8 ⇜ (27/2 → 14/1) ⇝ 117/8 | gain:1 note:D3 clip:0.1 ]", - "[ 213/16 ⇜ (27/2 → 14/1) ⇝ 225/16 | gain:0.06400000000000002 note:Bb6 clip:0.1 ]", - "[ 213/16 ⇜ (219/16 → 14/1) ⇝ 225/16 | gain:0.4 note:G4 clip:0.1 ]", - "[ 213/16 ⇜ (219/16 → 14/1) ⇝ 237/16 | gain:0.4 note:D4 clip:0.1 ]", - "[ 27/2 ⇜ (111/8 → 14/1) ⇝ 57/4 | gain:0.16000000000000003 note:G5 clip:0.1 ]", - "[ 27/2 ⇜ (111/8 → 14/1) ⇝ 15/1 | gain:0.16000000000000003 note:D5 clip:0.1 ]", - "[ (111/8 → 14/1) ⇝ 57/4 | s:hh gain:0.7 ]", - "[ (111/8 → 14/1) ⇝ 117/8 | gain:1 note:Bb3 clip:0.1 ]", - "[ 201/16 ⇜ (14/1 → 225/16) | gain:0.06400000000000002 note:D6 clip:0.1 ]", - "[ 213/16 ⇜ (14/1 → 225/16) | gain:0.4 note:G4 clip:0.1 ]", - "[ 213/16 ⇜ (14/1 → 225/16) | gain:0.06400000000000002 note:Bb6 clip:0.1 ]", - "[ 27/2 ⇜ (14/1 → 57/4) | gain:0.16000000000000003 note:G5 clip:0.1 ]", - "[ 111/8 ⇜ (14/1 → 57/4) | s:hh gain:0.7 ]", - "[ 105/8 ⇜ (14/1 → 117/8) | gain:1 note:D3 clip:0.1 ]", - "[ 111/8 ⇜ (14/1 → 117/8) | gain:1 note:Bb3 clip:0.1 ]", - "[ 213/16 ⇜ (14/1 → 237/16) | gain:0.4 note:D4 clip:0.1 ]", - "[ 27/2 ⇜ (14/1 → 15/1) | gain:0.16000000000000003 note:D5 clip:0.1 ]", + "[ 201/16 ⇜ (27/2 → 225/16) | gain:0.06400000000000002 note:D6 clip:0.1 ]", + "[ 213/16 ⇜ (27/2 → 225/16) | gain:0.06400000000000002 note:Bb6 clip:0.1 ]", + "[ 105/8 ⇜ (27/2 → 117/8) | gain:1 note:D3 clip:0.1 ]", + "[ 213/16 ⇜ (219/16 → 225/16) | gain:0.4 note:G4 clip:0.1 ]", + "[ 213/16 ⇜ (219/16 → 237/16) | gain:0.4 note:D4 clip:0.1 ]", + "[ 27/2 ⇜ (111/8 → 57/4) | gain:0.16000000000000003 note:G5 clip:0.1 ]", + "[ 111/8 → 57/4 | s:hh gain:0.7 ]", + "[ 111/8 → 117/8 | gain:1 note:Bb3 clip:0.1 ]", + "[ 27/2 ⇜ (111/8 → 15/1) | gain:0.16000000000000003 note:D5 clip:0.1 ]", "[ 219/16 ⇜ (225/16 → 231/16) | gain:0.06400000000000002 note:G6 clip:0.1 ]", "[ 225/16 → 237/16 | gain:0.4 note:Bb4 clip:0.1 ]", "[ 219/16 ⇜ (225/16 → 15/1) ⇝ 243/16 | gain:0.06400000000000002 note:D6 clip:0.1 ]", @@ -9643,29 +9295,18 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ 243/16 → 255/16 | gain:0.4 note:Ab4 clip:0.1 ]", "[ 123/8 → 63/4 | s:hh gain:0.7 ]", "[ 123/8 → 63/4 | gain:1 note:D2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 117/8 ⇜ (123/8 → 16/1) ⇝ 129/8 | gain:0.16000000000000003 note:C5 clip:0.1 ]", - "[ (123/8 → 16/1) ⇝ 129/8 | gain:0.16000000000000003 note:Ab5 clip:0.1 ]", + "[ 117/8 ⇜ (123/8 → 129/8) | gain:0.16000000000000003 note:C5 clip:0.1 ]", + "[ 123/8 → 129/8 | gain:0.16000000000000003 note:Ab5 clip:0.1 ]", "[ 249/16 → 255/16 | gain:1 note:D2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 237/16 ⇜ (249/16 → 16/1) ⇝ 261/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ (249/16 → 16/1) ⇝ 261/16 | gain:0.06400000000000002 note:Ab6 clip:0.1 ]", - "[ (63/4 → 16/1) ⇝ 129/8 | s:sn gain:0.7 ]", - "[ (63/4 → 16/1) ⇝ 129/8 | gain:1 note:Eb2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (63/4 → 16/1) ⇝ 33/2 | gain:1 note:F3 clip:0.1 ]", - "[ (63/4 → 16/1) ⇝ 69/4 | gain:1 note:C3 clip:0.1 ]", - "[ (255/16 → 16/1) ⇝ 261/16 | gain:1 note:Eb2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (255/16 → 16/1) ⇝ 267/16 | gain:0.4 note:F4 clip:0.1 ]", - "[ (255/16 → 16/1) ⇝ 279/16 | gain:0.4 note:C4 clip:0.1 ]", - "[ 117/8 ⇜ (16/1 → 129/8) | gain:0.16000000000000003 note:C5 clip:0.1 ]", - "[ 123/8 ⇜ (16/1 → 129/8) | gain:0.16000000000000003 note:Ab5 clip:0.1 ]", - "[ 63/4 ⇜ (16/1 → 129/8) | s:sn gain:0.7 ]", - "[ 63/4 ⇜ (16/1 → 129/8) | gain:1 note:Eb2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 237/16 ⇜ (16/1 → 261/16) | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ 249/16 ⇜ (16/1 → 261/16) | gain:0.06400000000000002 note:Ab6 clip:0.1 ]", - "[ 255/16 ⇜ (16/1 → 261/16) | gain:1 note:Eb2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 63/4 ⇜ (16/1 → 33/2) | gain:1 note:F3 clip:0.1 ]", - "[ 63/4 ⇜ (16/1 → 33/2) ⇝ 69/4 | gain:1 note:C3 clip:0.1 ]", - "[ 255/16 ⇜ (16/1 → 33/2) ⇝ 267/16 | gain:0.4 note:F4 clip:0.1 ]", - "[ 255/16 ⇜ (16/1 → 33/2) ⇝ 279/16 | gain:0.4 note:C4 clip:0.1 ]", + "[ 237/16 ⇜ (249/16 → 261/16) | gain:0.06400000000000002 note:C6 clip:0.1 ]", + "[ 249/16 → 261/16 | gain:0.06400000000000002 note:Ab6 clip:0.1 ]", + "[ 63/4 → 129/8 | s:sn gain:0.7 ]", + "[ 63/4 → 129/8 | gain:1 note:Eb2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ 63/4 → 33/2 | gain:1 note:F3 clip:0.1 ]", + "[ (63/4 → 33/2) ⇝ 69/4 | gain:1 note:C3 clip:0.1 ]", + "[ 255/16 → 261/16 | gain:1 note:Eb2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ (255/16 → 33/2) ⇝ 267/16 | gain:0.4 note:F4 clip:0.1 ]", + "[ (255/16 → 33/2) ⇝ 279/16 | gain:0.4 note:C4 clip:0.1 ]", "[ 129/8 → 33/2 | s:hh gain:0.7 ]", "[ (129/8 → 33/2) ⇝ 135/8 | gain:0.16000000000000003 note:F5 clip:0.1 ]", "[ (129/8 → 33/2) ⇝ 141/8 | gain:0.16000000000000003 note:C5 clip:0.1 ]", @@ -9678,24 +9319,15 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ 129/8 ⇜ (33/2 → 135/8) | gain:0.16000000000000003 note:F5 clip:0.1 ]", "[ 129/8 ⇜ (33/2 → 135/8) ⇝ 141/8 | gain:0.16000000000000003 note:C5 clip:0.1 ]", "[ 33/2 → 135/8 | s:bd gain:0.7 ]", - "[ 261/16 ⇜ (33/2 → 17/1) ⇝ 273/16 | gain:0.06400000000000002 note:F6 clip:0.1 ]", - "[ 261/16 ⇜ (33/2 → 17/1) ⇝ 285/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ 249/16 ⇜ (267/16 → 17/1) ⇝ 273/16 | gain:0.4 note:C4 clip:0.1 ]", - "[ 261/16 ⇜ (267/16 → 17/1) ⇝ 273/16 | gain:0.4 note:Ab4 clip:0.1 ]", - "[ 63/4 ⇜ (135/8 → 17/1) ⇝ 69/4 | gain:0.16000000000000003 note:C5 clip:0.1 ]", - "[ 33/2 ⇜ (135/8 → 17/1) ⇝ 69/4 | gain:0.16000000000000003 note:Ab5 clip:0.1 ]", - "[ (135/8 → 17/1) ⇝ 69/4 | s:hh gain:0.7 ]", - "[ (135/8 → 17/1) ⇝ 141/8 | gain:1 note:F3 clip:0.1 ]", - "[ (135/8 → 17/1) ⇝ 147/8 | gain:1 note:C3 clip:0.1 ]", - "[ 249/16 ⇜ (17/1 → 273/16) | gain:0.4 note:C4 clip:0.1 ]", - "[ 261/16 ⇜ (17/1 → 273/16) | gain:0.4 note:Ab4 clip:0.1 ]", - "[ 261/16 ⇜ (17/1 → 273/16) | gain:0.06400000000000002 note:F6 clip:0.1 ]", - "[ 261/16 ⇜ (17/1 → 273/16) ⇝ 285/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ 63/4 ⇜ (17/1 → 69/4) | gain:0.16000000000000003 note:C5 clip:0.1 ]", - "[ 33/2 ⇜ (17/1 → 69/4) | gain:0.16000000000000003 note:Ab5 clip:0.1 ]", - "[ 135/8 ⇜ (17/1 → 69/4) | s:hh gain:0.7 ]", - "[ 135/8 ⇜ (17/1 → 141/8) | gain:1 note:F3 clip:0.1 ]", - "[ 135/8 ⇜ (17/1 → 18/1) ⇝ 147/8 | gain:1 note:C3 clip:0.1 ]", + "[ 261/16 ⇜ (33/2 → 273/16) | gain:0.06400000000000002 note:F6 clip:0.1 ]", + "[ 261/16 ⇜ (33/2 → 273/16) ⇝ 285/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", + "[ 249/16 ⇜ (267/16 → 273/16) | gain:0.4 note:C4 clip:0.1 ]", + "[ 261/16 ⇜ (267/16 → 273/16) | gain:0.4 note:Ab4 clip:0.1 ]", + "[ 63/4 ⇜ (135/8 → 69/4) | gain:0.16000000000000003 note:C5 clip:0.1 ]", + "[ 33/2 ⇜ (135/8 → 69/4) | gain:0.16000000000000003 note:Ab5 clip:0.1 ]", + "[ 135/8 → 69/4 | s:hh gain:0.7 ]", + "[ 135/8 → 141/8 | gain:1 note:F3 clip:0.1 ]", + "[ (135/8 → 18/1) ⇝ 147/8 | gain:1 note:C3 clip:0.1 ]", "[ 255/16 ⇜ (273/16 → 279/16) | gain:0.06400000000000002 note:C6 clip:0.1 ]", "[ 267/16 ⇜ (273/16 → 279/16) | gain:0.06400000000000002 note:Ab6 clip:0.1 ]", "[ 273/16 → 285/16 | gain:0.4 note:F4 clip:0.1 ]", @@ -9717,35 +9349,24 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` "[ (18/1 → 147/8) ⇝ 75/4 | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", "[ 279/16 ⇜ (18/1 → 297/16) ⇝ 303/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", "[ 18/1 → 75/4 | gain:1 note:C3 clip:0.1 ]", - "[ (18/1 → 19/1) ⇝ 39/2 | gain:1 note:G2 clip:0.1 ]", + "[ 18/1 → 39/2 | gain:1 note:G2 clip:0.1 ]", "[ 291/16 → 147/8 | s:bd gain:0.7 ]", "[ 291/16 → 297/16 | gain:1 note:C2 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", "[ (291/16 → 297/16) ⇝ 303/16 | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", "[ 291/16 → 303/16 | gain:0.4 note:C4 clip:0.1 ]", - "[ (291/16 → 19/1) ⇝ 315/16 | gain:0.4 note:G3 clip:0.1 ]", + "[ (291/16 → 39/2) ⇝ 315/16 | gain:0.4 note:G3 clip:0.1 ]", "[ 147/8 → 75/4 | s:hh gain:0.7 ]", "[ 147/8 → 75/4 | gain:1 note:A1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (147/8 → 19/1) ⇝ 153/8 | gain:0.16000000000000003 note:C5 clip:0.1 ]", - "[ (147/8 → 19/1) ⇝ 159/8 | gain:0.16000000000000003 note:G4 clip:0.1 ]", + "[ 147/8 → 153/8 | gain:0.16000000000000003 note:C5 clip:0.1 ]", + "[ (147/8 → 39/2) ⇝ 159/8 | gain:0.16000000000000003 note:G4 clip:0.1 ]", "[ 297/16 → 303/16 | gain:1 note:A1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (297/16 → 19/1) ⇝ 309/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ (297/16 → 19/1) ⇝ 321/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ (75/4 → 19/1) ⇝ 153/8 | s:sn gain:0.7 ]", - "[ (75/4 → 19/1) ⇝ 153/8 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (75/4 → 19/1) ⇝ 39/2 | gain:1 note:Eb3 clip:0.1 ]", - "[ (303/16 → 19/1) ⇝ 309/16 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ (303/16 → 19/1) ⇝ 315/16 | gain:0.4 note:Eb4 clip:0.1 ]", - "[ 147/8 ⇜ (19/1 → 153/8) | gain:0.16000000000000003 note:C5 clip:0.1 ]", - "[ 75/4 ⇜ (19/1 → 153/8) | s:sn gain:0.7 ]", - "[ 75/4 ⇜ (19/1 → 153/8) | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 297/16 ⇜ (19/1 → 309/16) | gain:0.06400000000000002 note:C6 clip:0.1 ]", - "[ 303/16 ⇜ (19/1 → 309/16) | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", - "[ 18/1 ⇜ (19/1 → 39/2) | gain:1 note:G2 clip:0.1 ]", - "[ 291/16 ⇜ (19/1 → 39/2) ⇝ 315/16 | gain:0.4 note:G3 clip:0.1 ]", - "[ 147/8 ⇜ (19/1 → 39/2) ⇝ 159/8 | gain:0.16000000000000003 note:G4 clip:0.1 ]", - "[ 297/16 ⇜ (19/1 → 39/2) ⇝ 321/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", - "[ 75/4 ⇜ (19/1 → 39/2) | gain:1 note:Eb3 clip:0.1 ]", - "[ 303/16 ⇜ (19/1 → 39/2) ⇝ 315/16 | gain:0.4 note:Eb4 clip:0.1 ]", + "[ 297/16 → 309/16 | gain:0.06400000000000002 note:C6 clip:0.1 ]", + "[ (297/16 → 39/2) ⇝ 321/16 | gain:0.06400000000000002 note:G5 clip:0.1 ]", + "[ 75/4 → 153/8 | s:sn gain:0.7 ]", + "[ 75/4 → 153/8 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ 75/4 → 39/2 | gain:1 note:Eb3 clip:0.1 ]", + "[ 303/16 → 309/16 | gain:1 note:Bb1 s:square clip:0.4 cutoff:400 decay:0.12 sustain:0 ]", + "[ (303/16 → 39/2) ⇝ 315/16 | gain:0.4 note:Eb4 clip:0.1 ]", "[ 153/8 → 39/2 | s:hh gain:0.7 ]", "[ (153/8 → 39/2) ⇝ 159/8 | gain:0.16000000000000003 note:Eb5 clip:0.1 ]", "[ (309/16 → 39/2) ⇝ 321/16 | gain:0.06400000000000002 note:Eb6 clip:0.1 ]", @@ -9769,8 +9390,8 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = ` exports[`renders tunes > tune: waa2 1`] = ` [ - "[ -1/4 ⇜ (0/1 → 1/4) | note:48 clip:1.1738393178344886 s:sawtooth cutoff:3997.892048359052 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]", - "[ -1/4 ⇜ (0/1 → 1/4) | note:64 clip:1.1738393178344886 s:sawtooth cutoff:3997.892048359052 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]", + "[ -1/4 ⇜ (0/1 → 1/4) | note:48 clip:1.15 s:sawtooth cutoff:4000 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]", + "[ -1/4 ⇜ (0/1 → 1/4) | note:64 clip:1.15 s:sawtooth cutoff:4000 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]", "[ (0/1 → 1/4) ⇝ 1/2 | note:62 clip:1.197659880151613 s:sawtooth cutoff:3991.5732716763446 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]", "[ (0/1 → 1/4) ⇝ 1/2 | note:43 clip:1.197659880151613 s:sawtooth cutoff:3991.5732716763446 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]", "[ 0/1 ⇜ (1/4 → 1/2) | note:62 clip:1.197659880151613 s:square cutoff:3991.5732716763446 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]", @@ -9784,7 +9405,7 @@ exports[`renders tunes > tune: waa2 1`] = ` "[ 1/2 ⇜ (3/4 → 1/1) | note:69 clip:1.292380289809026 s:square cutoff:3924.645587531366 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]", "[ 3/4 → 1/1 | note:41 clip:1.315826773713709 s:square cutoff:3897.7021140702864 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]", "[ 3/4 → 1/1 | note:62 clip:1.315826773713709 s:square cutoff:3897.7021140702864 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]", - "[ (3/4 → 1/1) ⇝ 5/4 | note:81 clip:1.315826773713709 s:square cutoff:3897.7021140702864 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]", + "[ (3/4 → 1/1) ⇝ 5/4 | note:81 clip:1.3391427938628673 s:square cutoff:3866.789181894752 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]", ] `; From 16506c5ae2e73540ae8229e7e1cb195a60e76aa4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Mar 2024 01:58:30 +0100 Subject: [PATCH 58/69] improve scope memory footprint --- packages/codemirror/widget.mjs | 17 +++++++---------- packages/superdough/superdough.mjs | 1 + packages/transpiler/transpiler.mjs | 26 ++++++++++++++++++-------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index e2652327..16f123d7 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -1,10 +1,8 @@ import { StateEffect, StateField } from '@codemirror/state'; import { Decoration, EditorView, WidgetType } from '@codemirror/view'; -import { registerWidgetType } from '@strudel/transpiler'; +import { getWidgetID, registerWidgetType } from '@strudel/transpiler'; import { Pattern } from '@strudel/core'; -const getWidgetID = (from) => `widget_${from}`; - export const addWidget = StateEffect.define({ map: ({ from, to }, change) => { return { from: change.mapPos(from), to: change.mapPos(to) }; @@ -20,12 +18,12 @@ function getWidgets(widgetConfigs) { widgetConfigs // codemirror throws an error if we don't sort .sort((a, b) => a.to - b.to) - .map(({ to, type }) => { + .map((widgetConfig) => { return Decoration.widget({ - widget: new BlockWidget(to, type), + widget: new BlockWidget(widgetConfig), side: 0, block: true, - }).range(to); + }).range(widgetConfig.to); }) ); } @@ -62,16 +60,15 @@ export function setWidget(id, el) { } export class BlockWidget extends WidgetType { - constructor(col, type) { + constructor(widgetConfig) { super(); - this.col = col; - this.type = type; + this.widgetConfig = widgetConfig; } eq() { return true; } toDOM() { - const id = getWidgetID(this.col); // matches id generated in transpiler + const id = getWidgetID(this.widgetConfig); const el = widgetElements[id]; return el; } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index d0c276b0..cf9bd181 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -220,6 +220,7 @@ export let analysers = {}, export function getAnalyserById(id, fftSize = 1024) { if (!analysers[id]) { + // make sure this doesn't happen too often as it piles up garbage const analyserNode = getAudioContext().createAnalyser(); analyserNode.fftSize = fftSize; // getDestination().connect(analyserNode); diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 475b1e96..de06af06 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -53,12 +53,13 @@ export function transpiler(input, options = {}) { return this.replace(sliderWithLocation(node)); } if (isWidgetMethod(node)) { - emitWidgets && - widgets.push({ - to: node.end, - type: node.callee.property.name, - }); - return this.replace(widgetWithLocation(node)); + const widgetConfig = { + to: node.end, + index: widgets.length, + type: node.callee.property.name, + }; + emitWidgets && widgets.push(widgetConfig); + return this.replace(widgetWithLocation(node, widgetConfig)); } if (isBareSamplesCall(node, parent)) { return this.replace(withAwait(node)); @@ -140,8 +141,17 @@ function sliderWithLocation(node) { return node; } -function widgetWithLocation(node) { - const id = 'widget_' + node.end; +export function getWidgetID(widgetConfig) { + // the widget id is used as id for the dom element + as key for eventual resources + // for example, for each scope widget, a new analyser + buffer (large) is created + // that means, if we use the index index of line position as id, less garbage is generated + // return `widget_${widgetConfig.to}`; // more gargabe + //return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage + return `widget_${widgetConfig.index}`; // less garbage +} + +function widgetWithLocation(node, widgetConfig) { + const id = getWidgetID(widgetConfig); // add loc as identifier to first argument // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) node.arguments.unshift({ From d1c713fa2992102262652a0590a93292ce0e5893 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Mar 2024 02:29:48 +0100 Subject: [PATCH 59/69] make labeled statements look good on custom themes --- packages/codemirror/themes/algoboy.mjs | 1 + packages/codemirror/themes/blackscreen.mjs | 1 + packages/codemirror/themes/bluescreen.mjs | 1 + packages/codemirror/themes/strudel-theme.mjs | 1 + packages/codemirror/themes/teletext.mjs | 1 + packages/codemirror/themes/terminal.mjs | 1 + packages/codemirror/themes/whitescreen.mjs | 1 + 7 files changed, 7 insertions(+) diff --git a/packages/codemirror/themes/algoboy.mjs b/packages/codemirror/themes/algoboy.mjs index 399370e1..eba95b98 100644 --- a/packages/codemirror/themes/algoboy.mjs +++ b/packages/codemirror/themes/algoboy.mjs @@ -18,6 +18,7 @@ export default createTheme({ theme: 'light', settings, styles: [ + { tag: t.labelName, color: '#0f380f' }, { tag: t.keyword, color: '#0f380f' }, { tag: t.operator, color: '#0f380f' }, { tag: t.special(t.variableName), color: '#0f380f' }, diff --git a/packages/codemirror/themes/blackscreen.mjs b/packages/codemirror/themes/blackscreen.mjs index 135285a3..5671a6d8 100644 --- a/packages/codemirror/themes/blackscreen.mjs +++ b/packages/codemirror/themes/blackscreen.mjs @@ -15,6 +15,7 @@ export default createTheme({ theme: 'dark', settings, styles: [ + { tag: t.labelName, color: 'white' }, { tag: t.keyword, color: 'white' }, { tag: t.operator, color: 'white' }, { tag: t.special(t.variableName), color: 'white' }, diff --git a/packages/codemirror/themes/bluescreen.mjs b/packages/codemirror/themes/bluescreen.mjs index aa6489d6..eb04c411 100644 --- a/packages/codemirror/themes/bluescreen.mjs +++ b/packages/codemirror/themes/bluescreen.mjs @@ -18,6 +18,7 @@ export default createTheme({ theme: 'dark', settings, styles: [ + { tag: t.labelName, color: 'white' }, { tag: t.keyword, color: 'white' }, { tag: t.operator, color: 'white' }, { tag: t.special(t.variableName), color: 'white' }, diff --git a/packages/codemirror/themes/strudel-theme.mjs b/packages/codemirror/themes/strudel-theme.mjs index 4ae31060..4ec3ab92 100644 --- a/packages/codemirror/themes/strudel-theme.mjs +++ b/packages/codemirror/themes/strudel-theme.mjs @@ -15,6 +15,7 @@ export default createTheme({ gutterForeground: '#8a919966', }, styles: [ + { tag: t.labelName, color: '#89ddff' }, { tag: t.keyword, color: '#c792ea' }, { tag: t.operator, color: '#89ddff' }, { tag: t.special(t.variableName), color: '#eeffff' }, diff --git a/packages/codemirror/themes/teletext.mjs b/packages/codemirror/themes/teletext.mjs index 5fd9a557..ddbf7b33 100644 --- a/packages/codemirror/themes/teletext.mjs +++ b/packages/codemirror/themes/teletext.mjs @@ -27,6 +27,7 @@ export default createTheme({ theme: 'dark', settings, styles: [ + { tag: t.labelName, color: colorB }, { tag: t.keyword, color: colorA }, { tag: t.operator, color: mini }, { tag: t.special(t.variableName), color: colorA }, diff --git a/packages/codemirror/themes/terminal.mjs b/packages/codemirror/themes/terminal.mjs index 1374bb86..90c6e1d2 100644 --- a/packages/codemirror/themes/terminal.mjs +++ b/packages/codemirror/themes/terminal.mjs @@ -14,6 +14,7 @@ export default createTheme({ theme: 'dark', settings, styles: [ + { tag: t.labelName, color: '#41FF00' }, { tag: t.keyword, color: '#41FF00' }, { tag: t.operator, color: '#41FF00' }, { tag: t.special(t.variableName), color: '#41FF00' }, diff --git a/packages/codemirror/themes/whitescreen.mjs b/packages/codemirror/themes/whitescreen.mjs index 22abad9e..5abbb1ea 100644 --- a/packages/codemirror/themes/whitescreen.mjs +++ b/packages/codemirror/themes/whitescreen.mjs @@ -16,6 +16,7 @@ export default createTheme({ theme: 'light', settings, styles: [ + { tag: t.labelName, color: 'black' }, { tag: t.keyword, color: 'black' }, { tag: t.operator, color: 'black' }, { tag: t.special(t.variableName), color: 'black' }, From 6359bbe13917dc595a202a51935184f066efabe1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Mar 2024 03:14:05 +0100 Subject: [PATCH 60/69] transpile label statements to p calls --- packages/transpiler/transpiler.mjs | 33 +++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 72f2e851..b4bd5d62 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -9,7 +9,7 @@ export function transpiler(input, options = {}) { let ast = parse(input, { ecmaVersion: 2022, allowAwaitOutsideFunction: true, - locations: true, + locations: false, }); let miniLocations = []; @@ -49,6 +49,9 @@ export function transpiler(input, options = {}) { if (isBareSamplesCall(node, parent)) { return this.replace(withAwait(node)); } + if (isLabelStatement(node)) { + return this.replace(labelToP(node)); + } }, leave(node, parent, prop, index) {}, }); @@ -132,3 +135,31 @@ function withAwait(node) { argument: node, }; } + +function isLabelStatement(node) { + return node.type === 'LabeledStatement'; +} + +function labelToP(node) { + return { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: node.body.expression, + property: { + type: 'Identifier', + name: 'p', + }, + }, + arguments: [ + { + type: 'Literal', + value: node.label.name, + raw: `'${node.label.name}'`, + }, + ], + }, + }; +} From a71a167133361f7c3ee9f899510c2f7c00160942 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Mar 2024 03:16:17 +0100 Subject: [PATCH 61/69] fix: enable locations again --- packages/transpiler/transpiler.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index b4bd5d62..e59cd6b2 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -9,7 +9,7 @@ export function transpiler(input, options = {}) { let ast = parse(input, { ecmaVersion: 2022, allowAwaitOutsideFunction: true, - locations: false, + locations: true, }); let miniLocations = []; From 2de18846b007aabcd58a34715518a9b9045fa9d4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Mar 2024 03:17:47 +0100 Subject: [PATCH 62/69] add comment --- packages/transpiler/transpiler.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index e59cd6b2..a49b353d 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -140,6 +140,8 @@ function isLabelStatement(node) { return node.type === 'LabeledStatement'; } +// converts label expressions to p calls: "x: y" to "y.p('x')" +// see https://github.com/tidalcycles/strudel/issues/990 function labelToP(node) { return { type: 'ExpressionStatement', From 076b6f1c8205d5e5cf0c0b8075c3d3824d218cf9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Mar 2024 04:00:28 +0100 Subject: [PATCH 63/69] move canvas functions to codemirror package + fix id collisions --- packages/codemirror/widget.mjs | 37 ++++++++++++++++++++++++++++++ packages/transpiler/transpiler.mjs | 2 +- packages/widgets/canvas.mjs | 35 ---------------------------- packages/widgets/index.mjs | 1 - 4 files changed, 38 insertions(+), 37 deletions(-) delete mode 100644 packages/widgets/canvas.mjs diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 16f123d7..c2c1653b 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -90,3 +90,40 @@ export function registerWidget(type, fn) { }; } } + +// wire up @strudel/draw functions + +function getCanvasWidget(id, options = {}) { + const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options; + let canvas = document.getElementById(id) || document.createElement('canvas'); + console.log('canvas', canvas); + canvas.width = width * pixelRatio; + canvas.height = height * pixelRatio; + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; + setWidget(id, canvas); + return canvas; +} + +registerWidget('roll', (id, options = {}, pat) => { + const ctx = getCanvasWidget(id, options).getContext('2d'); + return pat.pianoroll({ fold: 1, ...options, ctx, id }); +}); + +registerWidget('twist', (id, options = {}, pat) => { + options = { width: 200, height: 200, size: 36, ...options }; + const ctx = getCanvasWidget(id, options).getContext('2d'); + return pat.spiral({ ...options, ctx, id }); +}); + +registerWidget('osci', (id, options = {}, pat) => { + options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options }; + const ctx = getCanvasWidget(id, options).getContext('2d'); + // TODO: find way to clear previous analysers to avoid memory leak + // .scope passes id to Pattern.analyze, which is picked up by superdough + // .. which calls getAnalyserById(analyze), creating a new analyzer (+buffer) for that key + // the id here is the col number where the osci function ends (as passed by the transpiler) + // effectively, this means for each evaluation of .osci on a unique col, a new analyser will be created + // the problem is that the old ones will never get deleted.. this might pile up some memory + return pat.scope({ ...options, ctx, id }); +}); diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 380d9ba0..d4b474d9 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -150,7 +150,7 @@ export function getWidgetID(widgetConfig) { // that means, if we use the index index of line position as id, less garbage is generated // return `widget_${widgetConfig.to}`; // more gargabe //return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage - return `widget_${widgetConfig.index}`; // less garbage + return `widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage } function widgetWithLocation(node, widgetConfig) { diff --git a/packages/widgets/canvas.mjs b/packages/widgets/canvas.mjs deleted file mode 100644 index d5a705b9..00000000 --- a/packages/widgets/canvas.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import { registerWidget, setWidget } from '@strudel/codemirror'; - -function getCanvasWidget(id, options = {}) { - const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options; - let canvas = document.getElementById(id) || document.createElement('canvas'); - canvas.width = width * pixelRatio; - canvas.height = height * pixelRatio; - canvas.style.width = width + 'px'; - canvas.style.height = height + 'px'; - setWidget(id, canvas); - return canvas; -} - -registerWidget('roll', (id, options = {}, pat) => { - const ctx = getCanvasWidget(id, options).getContext('2d'); - return pat.pianoroll({ fold: 1, ...options, ctx, id }); -}); - -registerWidget('twist', (id, options = {}, pat) => { - options = { width: 200, height: 200, size: 36, ...options }; - const ctx = getCanvasWidget(id, options).getContext('2d'); - return pat.spiral({ ...options, ctx, id }); -}); - -registerWidget('osci', (id, options = {}, pat) => { - options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options }; - const ctx = getCanvasWidget(id, options).getContext('2d'); - // TODO: find way to clear previous analysers to avoid memory leak - // .scope passes id to Pattern.analyze, which is picked up by superdough - // .. which calls getAnalyserById(analyze), creating a new analyzer (+buffer) for that key - // the id here is the col number where the osci function ends (as passed by the transpiler) - // effectively, this means for each evaluation of .osci on a unique col, a new analyser will be created - // the problem is that the old ones will never get deleted.. this might pile up some memory - return pat.scope({ ...options, ctx, id }); -}); diff --git a/packages/widgets/index.mjs b/packages/widgets/index.mjs index ad6e7d1f..281c6ab8 100644 --- a/packages/widgets/index.mjs +++ b/packages/widgets/index.mjs @@ -1,2 +1 @@ export * from './Claviature.jsx'; -export * from './canvas.mjs'; From be77882d70520e7d5d84be252b5c74bcf1321f77 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Mar 2024 04:15:59 +0100 Subject: [PATCH 64/69] rename inline functions to match global ones, prefixed with _ --- packages/codemirror/widget.mjs | 13 +++---------- packages/widgets/Claviature.jsx | 2 +- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index c2c1653b..f98dcf89 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -96,7 +96,6 @@ export function registerWidget(type, fn) { function getCanvasWidget(id, options = {}) { const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options; let canvas = document.getElementById(id) || document.createElement('canvas'); - console.log('canvas', canvas); canvas.width = width * pixelRatio; canvas.height = height * pixelRatio; canvas.style.width = width + 'px'; @@ -105,25 +104,19 @@ function getCanvasWidget(id, options = {}) { return canvas; } -registerWidget('roll', (id, options = {}, pat) => { +registerWidget('_pianoroll', (id, options = {}, pat) => { const ctx = getCanvasWidget(id, options).getContext('2d'); return pat.pianoroll({ fold: 1, ...options, ctx, id }); }); -registerWidget('twist', (id, options = {}, pat) => { +registerWidget('_spiral', (id, options = {}, pat) => { options = { width: 200, height: 200, size: 36, ...options }; const ctx = getCanvasWidget(id, options).getContext('2d'); return pat.spiral({ ...options, ctx, id }); }); -registerWidget('osci', (id, options = {}, pat) => { +registerWidget('_scope', (id, options = {}, pat) => { options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options }; const ctx = getCanvasWidget(id, options).getContext('2d'); - // TODO: find way to clear previous analysers to avoid memory leak - // .scope passes id to Pattern.analyze, which is picked up by superdough - // .. which calls getAnalyserById(analyze), creating a new analyzer (+buffer) for that key - // the id here is the col number where the osci function ends (as passed by the transpiler) - // effectively, this means for each evaluation of .osci on a unique col, a new analyser will be created - // the problem is that the old ones will never get deleted.. this might pile up some memory return pat.scope({ ...options, ctx, id }); }); diff --git a/packages/widgets/Claviature.jsx b/packages/widgets/Claviature.jsx index dc216d56..584dd09d 100644 --- a/packages/widgets/Claviature.jsx +++ b/packages/widgets/Claviature.jsx @@ -27,7 +27,7 @@ customElement('strudel-claviature', { options: '{}' }, (props, { element }) => { ); }); -registerWidget('claviature', (id, options = {}, pat) => { +registerWidget('_claviature', (id, options = {}, pat) => { options = { range: ['A0', 'C8'], scaleY: 1, scaleY: 0.5, scaleX: 0.5, ...options }; const height = (options.upperHeight + options.lowerHeight) * options.scaleY; const el = getSolidWidget('strudel-claviature', id, { ...options, height }); From 4319c43ceb4a85b67a4ceaa8090f3f7c1337b1e8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Mar 2024 04:31:30 +0100 Subject: [PATCH 65/69] comment out _spiral for now --- packages/codemirror/widget.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index f98dcf89..facfe1c2 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -109,11 +109,11 @@ registerWidget('_pianoroll', (id, options = {}, pat) => { return pat.pianoroll({ fold: 1, ...options, ctx, id }); }); -registerWidget('_spiral', (id, options = {}, pat) => { +/* registerWidget('_spiral', (id, options = {}, pat) => { options = { width: 200, height: 200, size: 36, ...options }; const ctx = getCanvasWidget(id, options).getContext('2d'); return pat.spiral({ ...options, ctx, id }); -}); +}); */ registerWidget('_scope', (id, options = {}, pat) => { options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options }; From b5312c27dc4e86cd53acafa9509c53bafa4a0bbe Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Mar 2024 04:36:48 +0100 Subject: [PATCH 66/69] move stuff for less changes --- packages/codemirror/slider.mjs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 21f744bb..72f95125 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -2,26 +2,9 @@ import { ref, pure } from '@strudel/core'; import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view'; import { StateEffect } from '@codemirror/state'; -export const setSliderWidgets = StateEffect.define(); - -export const updateSliderWidgets = (view, widgets) => { - view.dispatch({ effects: setSliderWidgets.of(widgets) }); -}; - export let sliderValues = {}; const getSliderID = (from) => `slider_${from}`; -function getSliders(widgetConfigs, view) { - return widgetConfigs - .filter((w) => w.type === 'slider') - .map(({ from, to, value, min, max, step }) => { - return Decoration.widget({ - widget: new SliderWidget(value, min, max, from, to, step, view), - side: 0, - }).range(from /* , to */); - }); -} - export class SliderWidget extends WidgetType { constructor(value, min, max, from, to, step, view) { super(); @@ -77,6 +60,23 @@ export class SliderWidget extends WidgetType { } } +export const setSliderWidgets = StateEffect.define(); + +export const updateSliderWidgets = (view, widgets) => { + view.dispatch({ effects: setSliderWidgets.of(widgets) }); +}; + +function getSliders(widgetConfigs, view) { + return widgetConfigs + .filter((w) => w.type === 'slider') + .map(({ from, to, value, min, max, step }) => { + return Decoration.widget({ + widget: new SliderWidget(value, min, max, from, to, step, view), + side: 0, + }).range(from /* , to */); + }); +} + export const sliderPlugin = ViewPlugin.fromClass( class { decorations; //: DecorationSet From dd16a1cbf4d0405088c6cd011d67edc47d6e656a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Mar 2024 04:48:25 +0100 Subject: [PATCH 67/69] hotfix: disable sync, it somehow breaks playback in the built version (pnpm build && pnpm preview) --- website/src/repl/Repl.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index f43669f6..675a1066 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -72,7 +72,7 @@ export function Repl({ embedded = false }) { }); }; const editor = new StrudelMirror({ - sync: true, + sync: false, defaultOutput: webaudioOutput, getTime: () => getAudioContext().currentTime, transpiler, From 95446a76ef73e4fb89a62c5b97ee41b3d77ba3ee Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 17 Mar 2024 12:22:24 +0100 Subject: [PATCH 68/69] breakout widgets package for now --- package.json | 1 - packages/widgets/Claviature.jsx | 45 ---------- packages/widgets/README.md | 31 ------- packages/widgets/index.mjs | 1 - packages/widgets/package.json | 42 --------- packages/widgets/solid.mjs | 13 --- packages/widgets/vite.config.js | 20 ----- pnpm-lock.yaml | 151 -------------------------------- website/package.json | 2 - website/src/repl/util.mjs | 1 - 10 files changed, 307 deletions(-) delete mode 100644 packages/widgets/Claviature.jsx delete mode 100644 packages/widgets/README.md delete mode 100644 packages/widgets/index.mjs delete mode 100644 packages/widgets/package.json delete mode 100644 packages/widgets/solid.mjs delete mode 100644 packages/widgets/vite.config.js diff --git a/package.json b/package.json index ea1373aa..741cf070 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "repl": "npm run prestart && cd website && npm run dev", "start": "npm run prestart && cd website && npm run dev", "dev": "npm run prestart && cd website && npm run dev", - "watch": "pnpm prestart && pnpm --parallel --filter {./website/**}... --filter \"@strudel/website\" watch", "build": "npm run prebuild && cd website && npm run build", "preview": "cd website && npm run preview", "osc": "cd packages/osc && npm run server", diff --git a/packages/widgets/Claviature.jsx b/packages/widgets/Claviature.jsx deleted file mode 100644 index 584dd09d..00000000 --- a/packages/widgets/Claviature.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import { For } from 'solid-js'; -import { customElement } from 'solid-element'; -import { getClaviature } from 'claviature'; -import { Dynamic } from 'solid-js/web'; -import { registerWidget } from '@strudel/codemirror'; -import { getSolidWidget } from './solid.mjs'; - -customElement('strudel-claviature', { options: '{}' }, (props, { element }) => { - let svg = () => { - let c = getClaviature({ - options: JSON.parse(props.options), - }); - return c; - }; - return ( - - - {(el) => { - return ( - - {el.value} - - ); - }} - - - ); -}); - -registerWidget('_claviature', (id, options = {}, pat) => { - options = { range: ['A0', 'C8'], scaleY: 1, scaleY: 0.5, scaleX: 0.5, ...options }; - const height = (options.upperHeight + options.lowerHeight) * options.scaleY; - const el = getSolidWidget('strudel-claviature', id, { ...options, height }); - return pat.onFrame(id, (haps) => { - const colorize = haps.map((h) => ({ keys: [h.value.note], color: h.context?.color || 'steelblue' })); - el.setAttribute( - 'options', - JSON.stringify({ - ...options, - range: options.range || ['A2', 'C6'], - colorize, - }), - ); - }); -}); diff --git a/packages/widgets/README.md b/packages/widgets/README.md deleted file mode 100644 index 53847c5d..00000000 --- a/packages/widgets/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# @strudel/widgets - -adds UI widgets to codemirror - -## claviature - -`Patter.claviature` renders a [claviature](https://www.npmjs.com/package/claviature). - -example usage: - -```js -chord("").voicing().piano() - .claviature() -``` - -All [claviature options](https://www.npmjs.com/package/claviature#options) will work. - -Here is an example that uses all available options: - -```js -chord("").voicing().piano() -.color('cyan') -.claviature({ - range: ['C1', 'C6'], // rendered note range - palette: ['cyan', 'magenta'], - stroke: 'black', - scaleX: 1, scaleY: 1, - upperHeight: 80, - lowerHeight: 50 -}) -``` diff --git a/packages/widgets/index.mjs b/packages/widgets/index.mjs deleted file mode 100644 index 281c6ab8..00000000 --- a/packages/widgets/index.mjs +++ /dev/null @@ -1 +0,0 @@ -export * from './Claviature.jsx'; diff --git a/packages/widgets/package.json b/packages/widgets/package.json deleted file mode 100644 index 775e4c17..00000000 --- a/packages/widgets/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "@strudel/widgets", - "version": "1.0.1", - "description": "Widget web components for Strudel", - "main": "dist/index.mjs", - "type": "module", - "scripts": { - "build": "vite build", - "watch": "vite build --watch", - "prepublishOnly": "npm run build" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/tidalcycles/strudel.git" - }, - "keywords": [ - "titdalcycles", - "strudel", - "pattern", - "livecoding", - "algorave" - ], - "author": "Felix Roos ", - "license": "AGPL-3.0-or-later", - "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" - }, - "homepage": "https://github.com/tidalcycles/strudel#readme", - "dependencies": { - "@strudel/core": "workspace:*", - "@strudel/transpiler": "workspace:*", - "@strudel/draw": "workspace:*", - "@strudel/codemirror": "workspace:*", - "claviature": "^0.1.0", - "solid-element": "^1.8.0", - "solid-js": "^1.8.15", - "vite-plugin-solid": "^2.10.1" - }, - "devDependencies": { - "vite": "^5.0.10" - } -} diff --git a/packages/widgets/solid.mjs b/packages/widgets/solid.mjs deleted file mode 100644 index b8403249..00000000 --- a/packages/widgets/solid.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import { setWidget } from '@strudel/codemirror'; - -export function getSolidWidget(type, id, options) { - let el = document.getElementById(id); - if (!el) { - el = document.createElement('div'); - const c = document.createElement(type); - el.appendChild(c); - } - el.height = options.height || 200; - setWidget(id, el); - return el?.firstChild; -} diff --git a/packages/widgets/vite.config.js b/packages/widgets/vite.config.js deleted file mode 100644 index 4ed5702c..00000000 --- a/packages/widgets/vite.config.js +++ /dev/null @@ -1,20 +0,0 @@ -import { defineConfig } from 'vite'; -import { dependencies } from './package.json'; -import { resolve } from 'path'; -import solid from 'vite-plugin-solid'; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [solid()], - build: { - lib: { - entry: resolve(__dirname, 'index.mjs'), - formats: ['es'], - fileName: (ext) => ({ es: 'index.mjs' })[ext], - }, - rollupOptions: { - external: [...Object.keys(dependencies)], - }, - target: 'esnext', - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00608001..033c6a7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -491,37 +491,6 @@ importers: specifier: ^5.0.10 version: 5.0.10 - packages/widgets: - dependencies: - '@strudel/codemirror': - specifier: workspace:* - version: link:../codemirror - '@strudel/core': - specifier: workspace:* - version: link:../core - '@strudel/draw': - specifier: workspace:* - version: link:../draw - '@strudel/transpiler': - specifier: workspace:* - version: link:../transpiler - claviature: - specifier: ^0.1.0 - version: 0.1.0 - solid-element: - specifier: ^1.8.0 - version: 1.8.0(solid-js@1.8.15) - solid-js: - specifier: ^1.8.15 - version: 1.8.15 - vite-plugin-solid: - specifier: ^2.10.1 - version: 2.10.1(solid-js@1.8.15)(vite@5.0.11) - devDependencies: - vite: - specifier: ^5.0.10 - version: 5.0.11(@types/node@20.10.6) - packages/xen: dependencies: '@strudel/core': @@ -624,9 +593,6 @@ importers: '@strudel/webaudio': specifier: workspace:* version: link:../packages/webaudio - '@strudel/widgets': - specifier: workspace:* - version: link:../packages/widgets '@strudel/xen': specifier: workspace:* version: link:../packages/xen @@ -1143,13 +1109,6 @@ packages: '@babel/types': 7.23.6 dev: true - /@babel/helper-module-imports@7.18.6: - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.23.6 - dev: false - /@babel/helper-module-imports@7.22.15: resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} engines: {node: '>=6.9.0'} @@ -5513,19 +5472,6 @@ packages: resolution: {integrity: sha512-3AN/9V/rKuv90NG65m4tTHsI04XrCKsWbztIcW7a8H5iIN7WlvWucRtVV0V/rT4QvtA11n5Vmp20fLwfMWqp6g==} dev: false - /babel-plugin-jsx-dom-expressions@0.37.17(@babel/core@7.23.7): - resolution: {integrity: sha512-1bv8rOTzs6TR3DVyVZ7ElxyPEhnS556FMWRIsB3gBPfkn/cSKaLvXLGk+X1lvI+SzcUo4G+UcmJrn3vr1ig8mQ==} - peerDependencies: - '@babel/core': ^7.20.12 - dependencies: - '@babel/core': 7.23.7 - '@babel/helper-module-imports': 7.18.6 - '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.23.7) - '@babel/types': 7.23.6 - html-entities: 2.3.3 - validate-html-nesting: 1.2.2 - dev: false - /babel-plugin-polyfill-corejs2@0.4.7(@babel/core@7.23.7): resolution: {integrity: sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==} peerDependencies: @@ -5562,15 +5508,6 @@ packages: - supports-color dev: true - /babel-preset-solid@1.8.15(@babel/core@7.23.7): - resolution: {integrity: sha512-P2yOQbB7Hn/m4YvpXV6ExHIMcgNWXWXcvY4kJzG3yqAB3hKS58OZRsvJ7RObsZWqXRvZTITBIwnpK0BMGu+ZIQ==} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.23.7 - babel-plugin-jsx-dom-expressions: 0.37.17(@babel/core@7.23.7) - dev: false - /bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -6180,10 +6117,6 @@ packages: dot-prop: 5.3.0 dev: true - /component-register@0.8.3: - resolution: {integrity: sha512-/0u8ov0WPWi2FL78rgB9aFOcfY8pJT4jP/l9NTOukGNLVQ6hk35sEJE1RkEnNQU3yk48Qr7HlDQjRQKEVfgeWg==} - dev: false - /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -8201,10 +8134,6 @@ packages: lru-cache: 10.1.0 dev: true - /html-entities@2.3.3: - resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} - dev: false - /html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -8727,11 +8656,6 @@ packages: call-bind: 1.0.5 dev: true - /is-what@4.1.16: - resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} - engines: {node: '>=12.13'} - dev: false - /is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -9698,13 +9622,6 @@ packages: yargs-parser: 20.2.9 dev: true - /merge-anything@5.1.7: - resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} - engines: {node: '>=12.13'} - dependencies: - is-what: 4.1.16 - dev: false - /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -12270,20 +12187,6 @@ packages: randombytes: 2.1.0 dev: true - /seroval-plugins@1.0.4(seroval@1.0.4): - resolution: {integrity: sha512-DQ2IK6oQVvy8k+c2V5x5YCtUa/GGGsUwUBNN9UqohrZ0rWdUapBFpNMYP1bCyRHoxOJjdKGl+dieacFIpU/i1A==} - engines: {node: '>=10'} - peerDependencies: - seroval: ^1.0 - dependencies: - seroval: 1.0.4 - dev: false - - /seroval@1.0.4: - resolution: {integrity: sha512-qQs/N+KfJu83rmszFQaTxcoJoPn6KNUruX4KmnmyD0oZkUoiNvJ1rpdYKDf4YHM05k+HOgCxa3yvf15QbVijGg==} - engines: {node: '>=10'} - dev: false - /server-destroy@1.0.1: resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==} @@ -12486,34 +12389,6 @@ packages: smart-buffer: 4.2.0 dev: true - /solid-element@1.8.0(solid-js@1.8.15): - resolution: {integrity: sha512-DG8HBCej5kNExUiFbVG8OFZojMGcLF8keXdGLEcHXBYtJ7zhm+a8HJnl5lfmBlTYGRk4ApgoBvlwH1ibg7quaQ==} - peerDependencies: - solid-js: ^1.8.0 - dependencies: - component-register: 0.8.3 - solid-js: 1.8.15 - dev: false - - /solid-js@1.8.15: - resolution: {integrity: sha512-d0QP/efr3UVcwGgWVPveQQ0IHOH6iU7yUhc2piy8arNG8wxKmvUy1kFxyF8owpmfCWGB87usDKMaVnsNYZm+Vw==} - dependencies: - csstype: 3.1.1 - seroval: 1.0.4 - seroval-plugins: 1.0.4(seroval@1.0.4) - dev: false - - /solid-refresh@0.6.3(solid-js@1.8.15): - resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} - peerDependencies: - solid-js: ^1.3 - dependencies: - '@babel/generator': 7.23.6 - '@babel/helper-module-imports': 7.22.15 - '@babel/types': 7.23.6 - solid-js: 1.8.15 - dev: false - /sort-array@4.1.5: resolution: {integrity: sha512-Ya4peoS1fgFN42RN1REk2FgdNOeLIEMKFGJvs7VTP3OklF8+kl2SkpVliZ4tk/PurWsrWRsdNdU+tgyOBkB9sA==} engines: {node: '>=10'} @@ -13629,10 +13504,6 @@ packages: hasBin: true dev: true - /validate-html-nesting@1.2.2: - resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} - dev: false - /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -13743,28 +13614,6 @@ packages: - supports-color dev: true - /vite-plugin-solid@2.10.1(solid-js@1.8.15)(vite@5.0.11): - resolution: {integrity: sha512-kfVdNLWaJqaJVL52U6iCCKNW/nXE7bS1VVGOWPGllOkJfcNILymVSY0LCBLSnyy0iYnRtrXpiHm14rMuzeC7CA==} - peerDependencies: - '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* - solid-js: ^1.7.2 - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - '@testing-library/jest-dom': - optional: true - dependencies: - '@babel/core': 7.23.7 - '@types/babel__core': 7.20.5 - babel-preset-solid: 1.8.15(@babel/core@7.23.7) - merge-anything: 5.1.7 - solid-js: 1.8.15 - solid-refresh: 0.6.3(solid-js@1.8.15) - vite: 5.0.11(@types/node@20.10.6) - vitefu: 0.2.5(vite@5.0.11) - transitivePeerDependencies: - - supports-color - dev: false - /vite@5.0.10: resolution: {integrity: sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==} engines: {node: ^18.0.0 || >=20.0.0} diff --git a/website/package.json b/website/package.json index 3fd05d54..7af9e61e 100644 --- a/website/package.json +++ b/website/package.json @@ -5,7 +5,6 @@ "private": true, "scripts": { "dev": "astro dev --host 0.0.0.0", - "watch": "astro dev --host 0.0.0.0", "start": "astro dev", "check": "astro check && tsc", "build": "astro build", @@ -25,7 +24,6 @@ "@heroicons/react": "^2.1.1", "@nanostores/persistent": "^0.9.1", "@nanostores/react": "^0.7.1", - "@strudel/widgets": "workspace:*", "@strudel/codemirror": "workspace:*", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 1df5a5b5..6dba7dab 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -82,7 +82,6 @@ export function loadModules() { import('@strudel/serial'), import('@strudel/soundfonts'), import('@strudel/csound'), - import('@strudel/widgets'), ]; if (isTauri()) { modules = modules.concat([ From 29cb6195b38f14fed3bf51efbeb079ec7977a7e3 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Mon, 18 Mar 2024 10:37:55 +0000 Subject: [PATCH 69/69] Fix pure mini highlight (#994) * preserve locations for pure values via mininotation * preserve weight --- packages/core/pattern.mjs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index a86290fd..649723a5 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -451,7 +451,9 @@ export class Pattern { * @noAutocomplete */ withHaps(func) { - return new Pattern((state) => func(this.query(state), state)); + const result = new Pattern((state) => func(this.query(state), state)); + result.weight = this.weight; + return result; } /** @@ -484,6 +486,7 @@ export class Pattern { const result = this.withHap((hap) => hap.setContext(func(hap.context))); if (this.__pure !== undefined) { result.__pure = this.__pure; + result.__pure_loc = this.__pure_loc; } return result; } @@ -510,10 +513,15 @@ export class Pattern { start, end, }; - return this.withContext((context) => { + const result = this.withContext((context) => { const locations = (context.locations || []).concat([location]); return { ...context, locations }; }); + if (this.__pure) { + result.__pure = this.__pure; + result.__pure_loc = location; + } + return result; } /** @@ -1608,7 +1616,12 @@ export function register(name, func, patternify = true, preserveWeight = false) if (firstArgs.every((arg) => arg.__pure != undefined)) { const pureArgs = firstArgs.map((arg) => arg.__pure); + const pureLocs = firstArgs.filter((arg) => arg.__pure_loc).map((arg) => arg.__pure_loc); result = func(...pureArgs, pat); + result = result.withContext((context) => { + const locations = (context.locations || []).concat(pureLocs); + return { ...context, locations }; + }); } else { const [left, ...right] = firstArgs;