Compare commits

..

1 Commits

Author SHA1 Message Date
Felix Roos f1a541caec fix: accurate midi scheduling on chromium 2026-08-12 20:48:01 +02:00
22 changed files with 140 additions and 375 deletions
+2 -9
View File
@@ -2352,18 +2352,11 @@ export const { offset } = registerControl('offset');
**/ **/
export const { octaves } = registerControl('octaves'); export const { octaves } = registerControl('octaves');
/** /**
* How the voicing is aligned to the anchor * Remove anchor note from the voicing. Useful for melody harmonization
* - `below`: top note <= anchor
* - `duck`: top note <= anchor, anchor excluded
* - `above`: bottom note >= anchor
* - `root`: bottom note is the lowest root of the chord >= anchor
*
* - `oldabove` : old (buggy) behavior of above, kept for legacy reason
* - `oldroot` : old (buggy) behavior of root, kept for legacy reason
* *
* @name mode * @name mode
* @tags tonal * @tags tonal
* @param {string | Pattern} modeName one of {below | above | duck | root | oldabove | oldroot} * @param {string | Pattern} modeName one of {below | above | duck | root}
* @example * @example
* mode("<below above duck root>").chord("C").voicing() * mode("<below above duck root>").chord("C").voicing()
* *
+9 -10
View File
@@ -1002,7 +1002,7 @@ export const arpWith = register('arpWith', (func, pat) => {
* */ * */
export const arp = register( export const arp = register(
'arp', 'arp',
(indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[_mod(i, haps.length)])), (indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])),
false, false,
); );
@@ -1529,9 +1529,7 @@ export function slowcat(...pats) {
// Array test here is to avoid infinite recursions.. // Array test here is to avoid infinite recursions..
pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat))); pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat)));
if (!pats.length) { if (pats.length == 1) {
return silence;
} else if (pats.length == 1) {
return pats[0]; return pats[0];
} }
@@ -1539,6 +1537,10 @@ export function slowcat(...pats) {
const span = state.span; const span = state.span;
const pat_n = _mod(span.begin.sam(), pats.length); const pat_n = _mod(span.begin.sam(), pats.length);
const pat = pats[pat_n]; const pat = pats[pat_n];
if (!pat) {
// pat_n can be negative, if the span is in the past..
return [];
}
// A bit of maths to make sure that cycles from constituent patterns aren't skipped. // A bit of maths to make sure that cycles from constituent patterns aren't skipped.
// For example if three patterns are slowcat-ed, the fourth cycle of the result should // For example if three patterns are slowcat-ed, the fourth cycle of the result should
// be the second (rather than fourth) cycle from the first pattern. // be the second (rather than fourth) cycle from the first pattern.
@@ -1555,14 +1557,11 @@ export function slowcat(...pats) {
* @return {Pattern} * @return {Pattern}
*/ */
export function slowcatPrime(...pats) { export function slowcatPrime(...pats) {
if (!pats.length) {
return silence;
}
pats = pats.map(reify); pats = pats.map(reify);
const query = function (state) { const query = function (state) {
const pat_n = _mod(Math.floor(state.span.begin), pats.length); const pat_n = Math.floor(state.span.begin) % pats.length;
const pat = pats[pat_n]; const pat = pats[pat_n]; // can be undefined for same cases e.g. /#cHVyZSg0MikKICAuZXZlcnkoMyxhZGQoNykpCiAgLmxhdGUoLjUp
return pat.query(state); return pat?.query(state) || [];
}; };
return new Pattern(query).splitQueries(); return new Pattern(query).splitQueries();
} }
+4 -21
View File
@@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
import { Hap } from './hap.mjs'; import { Hap } from './hap.mjs';
import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs'; import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs';
import { _mod } from './util.mjs';
import Fraction from './fraction.mjs'; import Fraction from './fraction.mjs';
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs'; import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
@@ -34,7 +33,7 @@ export const signal = (func) => {
* .scale('C major') * .scale('C major')
* *
*/ */
export const saw = signal((t) => _mod(t, 1)); export const saw = signal((t) => t % 1);
/** /**
* A sawtooth signal between -1 and 1 (like `saw`, but bipolar). * A sawtooth signal between -1 and 1 (like `saw`, but bipolar).
@@ -57,7 +56,7 @@ export const saw2 = saw.toBipolar();
* .scale('C major') * .scale('C major')
* *
*/ */
export const isaw = signal((t) => 1 - _mod(t, 1)); export const isaw = signal((t) => 1 - (t % 1));
/** /**
* A sawtooth signal between 1 and -1 (like `saw2`, but flipped). * A sawtooth signal between 1 and -1 (like `saw2`, but flipped).
@@ -114,7 +113,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
* n(square.segment(4).range(0,7)).scale("C:minor") * n(square.segment(4).range(0,7)).scale("C:minor")
* *
*/ */
export const square = signal((t) => Math.floor(_mod(t * 2, 2))); export const square = signal((t) => Math.floor((t * 2) % 2));
/** /**
* A square signal between -1 and 1 (like `square`, but bipolar). * A square signal between -1 and 1 (like `square`, but bipolar).
@@ -124,22 +123,6 @@ export const square = signal((t) => Math.floor(_mod(t * 2, 2)));
*/ */
export const square2 = square.toBipolar(); export const square2 = square.toBipolar();
/**
* A square signal between 1 and 0 (like `square` but flipped).
*
* @return {Pattern}
* @tags generators
*/
export const isquare = signal((t) => 1 - Math.floor(_mod(t * 2, 2)));
/**
* A square signal between 1 and -1 (like `isquare`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const isquare2 = isquare.toBipolar();
/** /**
* A triangle signal between 0 and 1. * A triangle signal between 0 and 1.
* *
@@ -413,7 +396,7 @@ export const randrun = (n) => {
.map((n, i) => [n, i]) .map((n, i) => [n, i])
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0])) .sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
.map((x) => x[1]); .map((x) => x[1]);
const i = _mod(t.cyclePos().mul(n).floor(), n); const i = t.cyclePos().mul(n).floor() % n;
return nums[i]; return nums[i];
})._segment(n); })._segment(n);
}; };
-49
View File
@@ -18,7 +18,6 @@ import {
fastcat, fastcat,
firstOf, firstOf,
slowcat, slowcat,
slowcatPrime,
cat, cat,
sequence, sequence,
palindrome, palindrome,
@@ -46,7 +45,6 @@ import {
rev, rev,
time, time,
run, run,
randrun,
binaryN, binaryN,
pick, pick,
stackLeft, stackLeft,
@@ -55,7 +53,6 @@ import {
stepcat, stepcat,
sometimes, sometimes,
expand, expand,
arp,
} from '../index.mjs'; } from '../index.mjs';
import { log, logValues } from '../pattern.mjs'; import { log, logValues } from '../pattern.mjs';
@@ -547,9 +544,6 @@ describe('Pattern', () => {
}); });
}); });
describe('slowcat()', () => { describe('slowcat()', () => {
it('Can be empty', () => {
expect(slowcat().firstCycle()).toStrictEqual([]);
});
it('Can concatenate things slowly', () => { it('Can concatenate things slowly', () => {
expect( expect(
slowcat('a', 'b') slowcat('a', 'b')
@@ -582,38 +576,6 @@ describe('Pattern', () => {
sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2)); sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2));
}); });
}); });
describe('slowcatPrime()', () => {
it('Can be empty', () => {
expect(slowcatPrime().firstCycle()).toStrictEqual([]);
});
it('Can slowcat patterns swapping back and forth skipping the expected notes', () => {
expect(
slowcatPrime(fastcat(0, 1, 2, 3).slow(2), fastcat(4, 5, 6, 7).slow(2))
.fast(4)
.firstCycle()
.map((a) => a.value),
).toStrictEqual([0, 1, 6, 7, 0, 1, 6, 7]);
});
it('Can go into negative time', () => {
expect(
slowcatPrime(fastcat(0, 1, 2, 3).slow(2), fastcat(4, 5, 6, 7).slow(2))
.fast(4)
.late(8)
.firstCycle()
.map((a) => a.value),
).toStrictEqual([0, 1, 6, 7, 0, 1, 6, 7]);
});
});
describe('arp()', () => {
it('It wraps around with both positive and negative indices', () => {
expect(
stack('a', 'b', 'c')
.arp(fastcat(-3, -2, -1, 0, 1, 2, 3, 4))
.firstCycle()
.map((a) => a.value),
).toStrictEqual(['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b']);
});
});
describe('rev()', () => { describe('rev()', () => {
it('Can reverse things', () => { it('Can reverse things', () => {
expect( expect(
@@ -983,17 +945,6 @@ describe('Pattern', () => {
it('Can run', () => { it('Can run', () => {
expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle()); expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
}); });
it('Can go into negative time', () => {
expect(run(4).late(1).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
});
});
describe('randrun', () => {
it('Can randrun', () => {
expect(randrun(4).firstCycle()).toStrictEqual(sequence(2, 1, 3, 0).firstCycle());
});
it('Can go into negative time', () => {
expect(randrun(4).late(1).firstCycle()).toStrictEqual(sequence(1, 2, 0, 3).firstCycle());
});
}); });
describe('binaryN', () => { describe('binaryN', () => {
it('Can make a binary pattern from a decimal', () => { it('Can make a binary pattern from a decimal', () => {
+6 -66
View File
@@ -8,23 +8,7 @@ import Fraction from 'fraction.js';
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
saw,
saw2,
isaw,
isaw2,
tri,
tri2,
itri,
itri2,
square,
square2,
isquare,
isquare2,
per,
perx,
cyclesPer,
} from '../signal.mjs';
import { fastcat, sequence, State, TimeSpan, Hap, note } from '../index.mjs'; import { fastcat, sequence, State, TimeSpan, Hap, note } from '../index.mjs';
const st = (begin, end) => new State(ts(begin, end)); const st = (begin, end) => new State(ts(begin, end));
@@ -40,61 +24,17 @@ const sameFirst = (a, b) => {
describe('signal()', () => { describe('signal()', () => {
it('Can make saw/saw2', () => { it('Can make saw/saw2', () => {
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0.25, 0.5, 0.75).firstCycle()); expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(),
);
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle()); expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
}); });
it('Can make isaw/isaw2', () => { it('Can make isaw/isaw2', () => {
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle()); expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle()); expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
}); });
it('Can make tri/tri2', () => {
expect(tri.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0.5, 1, 0.5).firstCycle());
expect(tri2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, 0, 1, 0).firstCycle());
});
it('Can make itri/itri2', () => {
expect(itri.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, 0.5).firstCycle());
expect(itri2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0, -1, 0).firstCycle());
});
it('Can make square/square2', () => {
expect(square.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0, 1, 1).firstCycle());
expect(square2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -1, 1, 1).firstCycle());
});
it('Can make isquare/isquare2', () => {
expect(isquare.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 1, 0, 0).firstCycle());
expect(isquare2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 1, -1, -1).firstCycle());
});
it('Can go into negative time', () => {
expect(saw.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(0, 0.25, 0.5, 0.75).firstCycle(),
);
expect(saw2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(-1, -0.5, 0, 0.5).firstCycle(),
);
expect(isaw.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(1, 0.75, 0.5, 0.25).firstCycle(),
);
expect(isaw2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(1, 0.5, 0, -0.5).firstCycle(),
);
expect(tri.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(0, 0.5, 1, 0.5).firstCycle(),
);
expect(tri2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, 0, 1, 0).firstCycle());
expect(itri.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(1, 0.5, 0, 0.5).firstCycle(),
);
expect(itri2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0, -1, 0).firstCycle());
expect(square.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0, 1, 1).firstCycle());
expect(square2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(-1, -1, 1, 1).firstCycle(),
);
expect(isquare.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(1, 1, 0, 0).firstCycle(),
);
expect(isquare2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(1, 1, -1, -1).firstCycle(),
);
});
}); });
describe('cyclesPer', () => { describe('cyclesPer', () => {
+1 -1
View File
@@ -137,7 +137,7 @@ export const getFrequency = (hap) => {
if (value.freq) { if (value.freq) {
return value.freq; return value.freq;
} }
return getFreq(value.note ?? value.n ?? value.value); return getFreq(value.note || value.n || value.value);
} }
if (typeof value === 'number' && context.type !== 'frequency') { if (typeof value === 'number' && context.type !== 'frequency') {
value = midiToFreq(hap.value); value = midiToFreq(hap.value);
+70 -60
View File
@@ -171,6 +171,21 @@ function normalize(value = 0, min = 0, max = 1, exp = 1) {
return Math.pow(normalized, exp); return Math.pow(normalized, exp);
} }
const isFirefox = navigator?.userAgent?.includes('Firefox');
// call fn either directly with given time (non-firefox) or after scheduleAtTime with undefined (firefox)
// the scheduleAtTime approach is still jittery, but the best we can be on firefox
// firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=2062997
function timedSend(timeMs, fn) {
if (isFirefox) {
const audioTime = getAudioContext().currentTime + (timeMs - performance.now()) / 1000;
scheduleAtTime(() => {
fn(undefined);
}, audioTime);
} else {
fn(timeMs);
}
}
function mapCC(mapping, value) { function mapCC(mapping, value) {
return Object.keys(value) return Object.keys(value)
.filter((key) => !!mapping[getControlName(key)]) .filter((key) => !!mapping[getControlName(key)])
@@ -182,7 +197,7 @@ function mapCC(mapping, value) {
} }
// sends a cc message to the given device on the given channel // sends a cc message to the given device on the given channel
function sendCC(ccn, ccv, device, midichan, targetTime) { function sendCC(ccn, ccv, device, midichan, timeMs) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) { if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1'); throw new Error('expected ccv to be a number between 0 and 1');
} }
@@ -190,23 +205,19 @@ function sendCC(ccn, ccv, device, midichan, targetTime) {
throw new Error('expected ccn to be a number or a string'); throw new Error('expected ccn to be a number or a string');
} }
const scaled = Math.round(ccv * 127); const scaled = Math.round(ccv * 127);
scheduleAtTime(() => { timedSend(timeMs, (timeMs) => device.sendControlChange(ccn, scaled, { channels: midichan, time: timeMs }));
device.sendControlChange(ccn, scaled, midichan);
}, targetTime);
} }
// sends a program change message to the given device on the given channel // sends a program change message to the given device on the given channel
function sendProgramChange(progNum, device, midichan, targetTime) { function sendProgramChange(progNum, device, midichan, timeMs) {
if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) { if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) {
throw new Error('expected progNum (program change) to be a number between 0 and 127'); throw new Error('expected progNum (program change) to be a number between 0 and 127');
} }
scheduleAtTime(() => { timedSend(timeMs, (timeMs) => device.sendProgramChange(progNum, { channels: midichan, time: timeMs }));
device.sendProgramChange(progNum, midichan);
}, targetTime);
} }
// sends a sysex message to the given device on the given channel // sends a sysex message to the given device on the given channel
function sendSysex(sysexid, sysexdata, device, targetTime) { function sendSysex(sysexid, sysexdata, device, timeMs) {
if (Array.isArray(sysexid)) { if (Array.isArray(sysexid)) {
if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all sysexid bytes must be integers between 0 and 255'); throw new Error('all sysexid bytes must be integers between 0 and 255');
@@ -221,13 +232,11 @@ function sendSysex(sysexid, sysexdata, device, targetTime) {
if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all sysex bytes must be integers between 0 and 255'); throw new Error('all sysex bytes must be integers between 0 and 255');
} }
scheduleAtTime(() => { timedSend(timeMs, (timeMs) => device.sendSysex(sysexid, sysexdata, { time: timeMs }));
device.sendSysex(sysexid, sysexdata);
}, targetTime);
} }
// sends a NRPN message to the given device on the given channel // sends a NRPN message to the given device on the given channel
function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) { function sendNRPN(nrpnn, nrpv, device, midichan, timeMs) {
if (Array.isArray(nrpnn)) { if (Array.isArray(nrpnn)) {
if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all nrpnn bytes must be integers between 0 and 255'); throw new Error('all nrpnn bytes must be integers between 0 and 255');
@@ -235,34 +244,29 @@ function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) {
} else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) { } else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) {
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers'); throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
} }
scheduleAtTime(() => {
device.sendNRPN(nrpnn, nrpv, midichan); timedSend(timeMs, (timeMs) => device.sendNrpnValue(nrpnn, nrpv, { channels: midichan, time: timeMs }));
}, targetTime);
} }
// sends a pitch bend message to the given device on the given channel // sends a pitch bend message to the given device on the given channel
function sendPitchBend(midibend, device, midichan, targetTime) { function sendPitchBend(midibend, device, midichan, timeMs) {
if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) { if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) {
throw new Error('expected midibend to be a number between -1 and 1'); throw new Error('expected midibend to be a number between -1 and 1');
} }
scheduleAtTime(() => { timedSend(timeMs, (timeMs) => device.sendPitchBend(midibend, { channels: midichan, time: timeMs }));
device.sendPitchBend(midibend, midichan);
}, targetTime);
} }
// sends a channel aftertouch message to the given device on the given channel // sends a channel aftertouch message to the given device on the given channel
function sendAftertouch(miditouch, device, midichan, targetTime) { function sendAftertouch(miditouch, device, midichan, timeMs) {
if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) { if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) {
throw new Error('expected miditouch to be a number between 0 and 1'); throw new Error('expected miditouch to be a number between 0 and 1');
} }
scheduleAtTime(() => { timedSend(timeMs, (timeMs) => device.sendChannelAftertouch(miditouch, { channels: midichan, time: timeMs }));
device.sendChannelAftertouch(miditouch, midichan);
}, targetTime);
} }
// sends a note message to the given device on the given channel // sends a note message to the given device on the given channel
function sendNote(note, velocity, duration, device, midichan, targetTime) { function sendNote(note, velocity, duration, device, midichan, timeMs) {
if (note == null || note === '') { if (note == null || note === '') {
throw new Error('note cannot be null or empty'); throw new Error('note cannot be null or empty');
} }
@@ -273,11 +277,10 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) {
throw new Error('duration must be a positive number'); throw new Error('duration must be a positive number');
} }
const midiNumber = typeof note === 'number' ? note : noteToMidi(note); const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration }); const midiNote = new Note(midiNumber, { attack: velocity });
scheduleAtTime(() => { timedSend(timeMs, (timeMs) => device.sendNoteOn(midiNote, { channels: midichan, time: timeMs }));
device.playNote(midiNote, midichan); timedSend(timeMs + duration, (timeMs) => device.sendNoteOff(midiNote, { channels: midichan, time: timeMs }));
}, targetTime);
} }
/** /**
@@ -287,8 +290,6 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) {
* @param {object} options Additional MIDI configuration options * @param {object} options Additional MIDI configuration options
* @example * @example
* note("c4").midichan(1).midi('IAC Driver Bus 1') * note("c4").midichan(1).midi('IAC Driver Bus 1')
* @example
* note("c4").midichan(1).midi('IAC Driver Bus 1', { controller: true, latency: 50 })
*/ */
Pattern.prototype.midi = function (midiport, options = {}) { Pattern.prototype.midi = function (midiport, options = {}) {
@@ -337,11 +338,28 @@ Pattern.prototype.midi = function (midiport, options = {}) {
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`), logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
}); });
return this.onTrigger((hap, _currentTime, cps, targetTime) => { let p; // filtered clock offset
let lastOffset;
return this.sortHapsByPart().onTrigger((hap, _currentTime, cps, targetTime) => {
if (!WebMidi.enabled) { if (!WebMidi.enabled) {
logger('Midi not enabled'); logger('Midi not enabled');
return; return;
} }
const { contextTime, performanceTime } = getAudioContext().getOutputTimestamp();
if (!contextTime || !performanceTime) {
logger('[midi] skip midi event: not ready yet?');
return;
}
const cutoff = 0.01;
const offset = performanceTime - contextTime * 1000;
p ??= offset; // first input is initial offset
if (offset !== lastOffset) {
// update filter only when offset changes
p = offset * cutoff + p * (1 - cutoff); // onepole iir filter to smooth drift :)
}
lastOffset = offset;
const timeMs = targetTime * 1000 + p; // this is now correct in performance time
hap.ensureObjectValue(); hap.ensureObjectValue();
// midi event values from hap with configurable defaults // midi event values from hap with configurable defaults
@@ -379,7 +397,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// if midimap is set, send a cc messages from defined controls // if midimap is set, send a cc messages from defined controls
if (midicontrolMap.has(midimap)) { if (midicontrolMap.has(midimap)) {
const ccs = mapCC(midicontrolMap.get(midimap), hap.value); const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, targetTime)); ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeMs));
} else if (midimap !== 'default') { } else if (midimap !== 'default') {
// Add warning when a non-existent midimap is specified // Add warning when a non-existent midimap is specified
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`); logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
@@ -388,15 +406,17 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// Handle note // Handle note
if (note !== undefined && !midiConfig.isController) { if (note !== undefined && !midiConfig.isController) {
// note off messages will often a few ms arrive late, // note off messages will often a few ms arrive late,
// try to prevent glitching by subtracting noteOffsetMs from the duration length // try to prevent glitching by subtracting at max noteOffsetMs from the duration length
const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs; const hapDuration = (hap.duration.valueOf() / cps) * 1000;
const offset = Math.min(midiConfig.noteOffsetMs, hapDuration / 2);
const duration = hapDuration - offset;
sendNote(note, velocity, duration, device, midichan, targetTime); sendNote(note, velocity, duration, device, midichan, timeMs);
} }
// Handle program change // Handle program change
if (progNum !== undefined) { if (progNum !== undefined) {
sendProgramChange(progNum, device, midichan, targetTime); sendProgramChange(progNum, device, midichan, timeMs);
} }
// Handle sysex // Handle sysex
@@ -406,63 +426,53 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// if sysexid is an array the first byte is 0x00 // if sysexid is an array the first byte is 0x00
if (sysexid !== undefined && sysexdata !== undefined) { if (sysexid !== undefined && sysexdata !== undefined) {
sendSysex(sysexid, sysexdata, device, targetTime); sendSysex(sysexid, sysexdata, device, timeMs);
} }
// Handle control change // Handle control change
if (ccv !== undefined && ccn !== undefined) { if (ccv !== undefined && ccn !== undefined) {
sendCC(ccn, ccv, device, midichan, targetTime); sendCC(ccn, ccv, device, midichan, timeMs);
} }
// Handle NRPN non-registered parameter number // Handle NRPN non-registered parameter number
if (nrpnn !== undefined && nrpv !== undefined) { if (nrpnn !== undefined && nrpv !== undefined) {
sendNRPN(nrpnn, nrpv, device, midichan, targetTime); sendNRPN(nrpnn, nrpv, device, midichan, timeMs);
} }
// Handle midibend // Handle midibend
if (midibend !== undefined) { if (midibend !== undefined) {
sendPitchBend(midibend, device, midichan, targetTime); sendPitchBend(midibend, device, midichan, timeMs);
} }
// Handle miditouch // Handle miditouch
if (miditouch !== undefined) { if (miditouch !== undefined) {
sendAftertouch(miditouch, device, midichan, targetTime); sendAftertouch(miditouch, device, midichan, timeMs);
} }
// Handle midicmd // Handle midicmd
if (hap.whole.begin + 0 === 0) { if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info // we need to start here because we have the timing info
scheduleAtTime(() => { timedSend(timeMs, (timeMs) => device.sendStart({ time: timeMs }));
device.sendStart();
}, targetTime);
} }
if (['clock', 'midiClock'].includes(midicmd)) { if (['clock', 'midiClock'].includes(midicmd)) {
scheduleAtTime(() => { timedSend(timeMs, (timeMs) => device.sendClock({ time: timeMs }));
device.sendClock();
}, targetTime);
} else if (['start'].includes(midicmd)) { } else if (['start'].includes(midicmd)) {
scheduleAtTime(() => { timedSend(timeMs, (timeMs) => device.sendStart({ time: timeMs }));
device.sendStart();
}, targetTime);
} else if (['stop'].includes(midicmd)) { } else if (['stop'].includes(midicmd)) {
scheduleAtTime(() => { timedSend(timeMs, (timeMs) => device.sendStop({ time: timeMs }));
device.sendStop();
}, targetTime);
} else if (['continue'].includes(midicmd)) { } else if (['continue'].includes(midicmd)) {
scheduleAtTime(() => { timedSend(timeMs, (timeMs) => device.sendContinue({ time: timeMs }));
device.sendContinue();
}, targetTime);
} else if (Array.isArray(midicmd)) { } else if (Array.isArray(midicmd)) {
if (midicmd[0] === 'progNum') { if (midicmd[0] === 'progNum') {
sendProgramChange(midicmd[1], device, midichan, targetTime); sendProgramChange(midicmd[1], device, midichan, timeMs);
} else if (midicmd[0] === 'cc') { } else if (midicmd[0] === 'cc') {
if (midicmd.length === 2) { if (midicmd.length === 2) {
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, targetTime); sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeMs);
} }
} else if (midicmd[0] === 'sysex') { } else if (midicmd[0] === 'sysex') {
if (midicmd.length === 3) { if (midicmd.length === 3) {
const [_, id, data] = midicmd; const [_, id, data] = midicmd;
sendSysex(id, data, device, targetTime); sendSysex(id, data, device, timeMs);
} }
} }
} }
+1
View File
@@ -45,6 +45,7 @@ export function parseControlsFromHap(hap, cps) {
controls.midinote = parseNumeral(controls.note); controls.midinote = parseNumeral(controls.note);
} }
} }
controls.bank && (controls.s = controls.bank + controls.s);
controls.roomsize && (controls.size = parseNumeral(controls.roomsize)); controls.roomsize && (controls.size = parseNumeral(controls.roomsize));
// speed adjustment for CPS is handled on the DSP side in superdirt and pattern side in Strudel, // speed adjustment for CPS is handled on the DSP side in superdirt and pattern side in Strudel,
// so we need to undo the adjustment before sending the message to superdirt. // so we need to undo the adjustment before sending the message to superdirt.
-1
View File
@@ -98,7 +98,6 @@ if (args.includes('--json')) {
} }
writeFileSync(outFile, json, 'utf8'); writeFileSync(outFile, json, 'utf8');
console.log(`Wrote json to ${outFile}`); console.log(`Wrote json to ${outFile}`);
process.exit(0);
} }
console.log( console.log(
+2 -9
View File
@@ -7,21 +7,14 @@ Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/
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 <https://www.gnu.org/licenses/>. 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 <https://www.gnu.org/licenses/>.
*/ */
import { clearNodePool } from './nodePools.mjs';
let audioContext; let audioContext;
export const setDefaultAudioContext = () => { export const setDefaultAudioContext = () => {
return setAudioContext(new AudioContext()); audioContext = new AudioContext();
return audioContext;
}; };
export const setAudioContext = (context) => { export const setAudioContext = (context) => {
// Existing nodes in the node pool contain references to the previous AudioContext,
// so all the nodes in the pool must be cleared when we set a new AudioContext.
clearNodePool();
if (audioContext && audioContext.state !== 'closed') {
audioContext.close();
}
audioContext = context; audioContext = context;
return audioContext; return audioContext;
}; };
+3 -3
View File
@@ -6,8 +6,8 @@ import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle']; export const noises = ['pink', 'white', 'brown', 'crackle'];
export function gainNode(value, audioContext = getAudioContext()) { export function gainNode(value) {
const node = audioContext.createGain(); const node = getAudioContext().createGain();
node.gain.value = value; node.gain.value = value;
return node; return node;
} }
@@ -374,7 +374,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
// Certain browsers requires audio nodes to be connected in order for their onended events // Certain browsers requires audio nodes to be connected in order for their onended events
// to fire, so we _mute it_ and then connect it to the destination // to fire, so we _mute it_ and then connect it to the destination
const zeroGain = gainNode(0, audioContext); const zeroGain = gainNode(0);
zeroGain.connect(audioContext.destination); zeroGain.connect(audioContext.destination);
constantNode.connect(zeroGain); constantNode.connect(zeroGain);
-13
View File
@@ -5,24 +5,11 @@ Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/
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 <https://www.gnu.org/licenses/>. 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 <https://www.gnu.org/licenses/>.
*/ */
import { releaseAudioNode } from './helpers.mjs';
const nodePools = new Map(); const nodePools = new Map();
const POOL_KEY = Symbol('nodePoolKey'); const POOL_KEY = Symbol('nodePoolKey');
export const isPoolable = (node) => !!node[POOL_KEY]; export const isPoolable = (node) => !!node[POOL_KEY];
export const clearNodePool = () => {
for (const pool of nodePools) {
for (const node of pool) {
if (node instanceof AudioNode) {
releaseAudioNode(node);
}
}
}
nodePools.clear();
};
const getNodeTime = (node) => { const getNodeTime = (node) => {
return node.context?.currentTime ?? 0; return node.context?.currentTime ?? 0;
}; };
+1 -8
View File
@@ -137,14 +137,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
const interval = !isNaN(Number(intervalOrSemitones)) const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones) ? Interval.fromSemitones(intervalOrSemitones)
: String(intervalOrSemitones); : String(intervalOrSemitones);
const targetNote = Note.transpose(note, interval);
let n = Note.get(note);
if (n.oct == undefined) {
n.oct = 3;
let n1 = Note.get(Note.transpose(n, interval));
n.oct = n1.oct == 3 ? undefined : 3;
}
const targetNote = Note.transpose(n, interval);
if (typeof hap.value === 'object') { if (typeof hap.value === 'object') {
return hap.withValue(() => ({ ...hap.value, note: targetNote })); return hap.withValue(() => ({ ...hap.value, note: targetNote }));
} }
+3 -14
View File
@@ -134,16 +134,6 @@ let modeTarget = {
duck: (v) => v.slice(-1)[0], duck: (v) => v.slice(-1)[0],
above: (v) => v[0], above: (v) => v[0],
root: (v) => v[0], root: (v) => v[0],
oldabove: (v) => v[0],
oldroot: (v) => v[0],
};
let modeMult = {
below: 1,
duck: 1,
above: -1,
root: -1,
oldabove: 1,
oldroot: 1,
}; };
export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5', octaves = 1 }) { export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5', octaves = 1 }) {
@@ -154,20 +144,19 @@ export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below'
const voicings = dictionary[symbol].map((voicing) => const voicings = dictionary[symbol].map((voicing) =>
(typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones), (typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones),
); );
const mult = modeMult[mode];
let minDistance, bestIndex; let minDistance, bestIndex;
// calculate distances up from voicing top notes // calculate distances up from voicing top notes
let chromaDiffs = voicings.map((v, i) => { let chromaDiffs = voicings.map((v, i) => {
const targetStep = modeTarget[mode](v); const targetStep = modeTarget[mode](v);
const diff = _mod((anchorChroma - targetStep - rootChroma) * mult, 12); const diff = _mod(anchorChroma - targetStep - rootChroma, 12);
if (minDistance === undefined || diff < minDistance) { if (minDistance === undefined || diff < minDistance) {
minDistance = diff; minDistance = diff;
bestIndex = i; bestIndex = i;
} }
return diff * mult; return diff;
}); });
if (mode === 'root' || mode === 'oldroot') { if (mode === 'root') {
bestIndex = 0; bestIndex = 0;
} }
-3
View File
@@ -185,9 +185,6 @@ export const rootNotes = register('rootNotes', function (octave, pat) {
* - `below`: top note <= anchor * - `below`: top note <= anchor
* - `duck`: top note <= anchor, anchor excluded * - `duck`: top note <= anchor, anchor excluded
* - `above`: bottom note >= anchor * - `above`: bottom note >= anchor
* - `root`: bottom note is the lowest root of the chord >= anchor
* - `oldabove` : old (buggy) behavior of above, kept for legacy reason
* - `oldroot` : old (buggy) behavior of root, kept for legacy reason
* - `offset`: whole number that shifts the voicing up or down to the next voicing * - `offset`: whole number that shifts the voicing up or down to the next voicing
* - `n`: if set, the voicing is played like a scale. Overshooting numbers will be octaved * - `n`: if set, the voicing is played like a scale. Overshooting numbers will be octaved
* *
+25 -85
View File
@@ -56,14 +56,32 @@ export async function renderPatternAudio(
maxPolyphony, maxPolyphony,
multiChannelOrbits, multiChannelOrbits,
}); });
logger('[webaudio] preloading');
// Firefox currently doesn't support suspending an OfflineAudioContext, // Calling superdough(...) in ascending onset time order is important
// so no chunked rendering. Bad performance, but at least it works. // for controls that depend on the audio graph state like `cut`
return ( let haps = pattern
audioContext.suspend === undefined .queryArc(begin, end, { _cps: cps })
? renderPatternAudioWhole(audioContext, pattern, cps, begin, end) .sort((a, b) => a.whole.begin.valueOf() - b.whole.begin.valueOf());
: renderPatternAudioInChunks(audioContext, pattern, cps, begin, end, 1) for (const hap of haps) {
) if (hap.hasOnset()) {
try {
await superdough(
hap2value(hap),
(hap.whole.begin.valueOf() - begin) / cps,
hap.duration / cps,
cps,
(hap.whole?.begin.valueOf() - begin) / cps,
);
} catch (err) {
errorLogger(err, 'webaudio');
}
}
}
logger('[webaudio] start rendering');
return audioContext
.startRendering()
.then((renderedBuffer) => { .then((renderedBuffer) => {
const wavBuffer = audioBufferToWav(renderedBuffer); const wavBuffer = audioBufferToWav(renderedBuffer);
const blob = new Blob([wavBuffer], { type: 'audio/wav' }); const blob = new Blob([wavBuffer], { type: 'audio/wav' });
@@ -84,84 +102,6 @@ export async function renderPatternAudio(
}); });
} }
async function renderPatternAudioWhole(audioContext, pattern, cps, begin, end) {
logger(`[webaudio] preloading`);
await scheduleHapsChunk(pattern, cps, begin, begin, end);
logger('[webaudio] start rendering');
return audioContext.startRendering();
}
async function renderPatternAudioInChunks(audioContext, pattern, cps, begin, end, chunkSizeInCycles) {
let currentCycle = begin;
let renderPromise = null;
logger('[webaudio] start rendering');
while (currentCycle <= end) {
const chunkStart = currentCycle;
const chunkEnd = Math.min(currentCycle + chunkSizeInCycles, end);
logger(`[webaudio] preloading cycles ${chunkStart} - ${chunkEnd}`);
await scheduleHapsChunk(pattern, cps, begin, chunkStart, chunkEnd);
logger(`[webaudio] rendering cycles ${chunkStart} - ${chunkEnd}`);
currentCycle += chunkSizeInCycles;
// According to the MDN docs, suspends should be scheduled while
// the audioContext is not currently running for better precision.
// So we schedule the suspend first, and await after resuming.
var suspendPromise;
if (currentCycle < end) {
// Make sure to suspend one cycle before the next currentCycle
// so the next haps can be scheduled on time.
suspendPromise = audioContext.suspend((currentCycle - begin - 1) / cps);
}
if (renderPromise === null) {
renderPromise = audioContext.startRendering();
} else {
await audioContext.resume();
}
if (currentCycle < end) {
await suspendPromise;
}
}
logger('[webaudio] finish rendering');
return renderPromise;
}
async function scheduleHapsChunk(pattern, cps, begin, chunkStart, chunkEnd) {
// Calling superdough(...) in ascending onset time order is important
// for controls that depend on the audio graph state like `cut`
let haps = pattern
.queryArc(chunkStart, chunkEnd, { _cps: cps })
.sort((a, b) => a.whole.begin.valueOf() - b.whole.begin.valueOf());
for (const hap of haps) {
if (hap.hasOnset()) {
try {
await superdough(
hap2value(hap),
(hap.whole.begin.valueOf() - begin) / cps,
hap.duration / cps,
cps,
(hap.whole?.begin.valueOf() - begin) / cps,
);
} catch (err) {
errorLogger(err, 'webaudio');
}
}
}
}
export function webaudioRepl(options = {}) { export function webaudioRepl(options = {}) {
const audioContext = options.audioContext ?? getAudioContext(); const audioContext = options.audioContext ?? getAudioContext();
setAudioContext(audioContext); setAudioContext(audioContext);
+8 -8
View File
@@ -639,30 +639,30 @@ exports[`runs examples > example "K" example index 0 1`] = `
"[ 7/4 → 11/6 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]", "[ 7/4 → 11/6 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
"[ 11/6 → 23/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]", "[ 11/6 → 23/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
"[ 23/12 → 2/1 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]", "[ 23/12 → 2/1 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
"[ 2/1 → 25/12 | note:Db5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]", "[ 2/1 → 25/12 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
"[ 25/12 → 13/6 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]", "[ 25/12 → 13/6 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
"[ 13/6 → 9/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]", "[ 13/6 → 9/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
"[ 9/4 → 7/3 | note:Db5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]", "[ 9/4 → 7/3 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
"[ (7/3 → 19/8) ⇝ 29/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]", "[ (7/3 → 19/8) ⇝ 29/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
"[ 7/3 ⇜ (19/8 → 29/12) | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]", "[ 7/3 ⇜ (19/8 → 29/12) | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
"[ 29/12 → 5/2 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]", "[ 29/12 → 5/2 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
"[ 5/2 → 31/12 | note:Db5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]", "[ 5/2 → 31/12 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
"[ 31/12 → 8/3 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]", "[ 31/12 → 8/3 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
"[ 8/3 → 11/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]", "[ 8/3 → 11/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
"[ 11/4 → 17/6 | note:Db5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]", "[ 11/4 → 17/6 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
"[ 17/6 → 35/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]", "[ 17/6 → 35/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
"[ 35/12 → 3/1 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]", "[ 35/12 → 3/1 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
"[ 3/1 → 37/12 | note:Eb5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]", "[ 3/1 → 37/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
"[ 37/12 → 19/6 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]", "[ 37/12 → 19/6 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
"[ 19/6 → 13/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]", "[ 19/6 → 13/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
"[ 13/4 → 10/3 | note:Eb5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]", "[ 13/4 → 10/3 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
"[ (10/3 → 27/8) ⇝ 41/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]", "[ (10/3 → 27/8) ⇝ 41/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
"[ 10/3 ⇜ (27/8 → 41/12) | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]", "[ 10/3 ⇜ (27/8 → 41/12) | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
"[ 41/12 → 7/2 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]", "[ 41/12 → 7/2 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
"[ 7/2 → 43/12 | note:Eb5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]", "[ 7/2 → 43/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
"[ 43/12 → 11/3 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]", "[ 43/12 → 11/3 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
"[ 11/3 → 15/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]", "[ 11/3 → 15/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
"[ 15/4 → 23/6 | note:Eb5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]", "[ 15/4 → 23/6 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
"[ 23/6 → 47/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]", "[ 23/6 → 47/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
"[ 47/12 → 4/1 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]", "[ 47/12 → 4/1 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
] ]
-4
View File
@@ -7203,12 +7203,8 @@ exports[`renders tunes > tune: holyflute 1`] = `
exports[`renders tunes > tune: juxUndTollerei 1`] = ` exports[`renders tunes > tune: juxUndTollerei 1`] = `
[ [
"[ -99/200 ⇜ (0/1 → 1/200) | note:63 s:triangle pan:0 cutoff:758.852817928549 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ -99/200 ⇜ (0/1 → 1/200) | note:67 s:triangle pan:1 color:green cutoff:758.852817928549 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 0/1 → 1/4 | note:c3 s:sawtooth pan:0 cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 0/1 → 1/4 | note:c3 s:sawtooth pan:0 cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 0/1 → 1/4 | note:bb3 s:sawtooth pan:1 color:green cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 0/1 → 1/4 | note:bb3 s:sawtooth pan:1 color:green cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/200 → 101/200 | note:55 s:triangle pan:0 cutoff:1103.534282651425 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/200 → 101/200 | note:65 s:triangle pan:1 color:green cutoff:1103.534282651425 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/4 → 1/2 | note:eb3 s:sawtooth pan:0 cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 1/4 → 1/2 | note:eb3 s:sawtooth pan:0 cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/4 → 1/2 | note:g3 s:sawtooth pan:1 color:green cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 1/4 → 1/2 | note:g3 s:sawtooth pan:1 color:green cutoff:1275.581289814515 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:1444.415089128581 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:1444.415089128581 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
+2
View File
@@ -150,3 +150,5 @@ In the future, the integration could be improved by passing all patterned contro
This could work by a unique [channel](https://kunstmusik.github.io/icsc2022-csound-web/tutorial2-interacting-with-csound/#step-4---writing-continuous-data-channels) This could work by a unique [channel](https://kunstmusik.github.io/icsc2022-csound-web/tutorial2-interacting-with-csound/#step-4---writing-continuous-data-channels)
for each value. Channels could be read [like this](https://github.com/csound/csound/blob/master/Android/CsoundForAndroid/CsoundAndroidExamples/src/main/res/raw/multitouch_xy.csd). for each value. Channels could be read [like this](https://github.com/csound/csound/blob/master/Android/CsoundForAndroid/CsoundAndroidExamples/src/main/res/raw/multitouch_xy.csd).
Also, it might make sense to have a standard library of csound instruments for strudel's effects. Also, it might make sense to have a standard library of csound instruments for strudel's effects.
Now, let's dive into the [Functional JavaScript API](/functions/intro)
+1 -1
View File
@@ -45,7 +45,7 @@ Here, the LFO will modulate the low pass filter `.lpf`.
## Moving away from the default ## Moving away from the default
The following sections explain how to pass parameters to `.lfo`. Similar to `._spectrum` above, almost all the configuration of `lfo` lives inside a json object, starting with `{` and ending with `}`. The following sections explain how pass parameters to `.lfo`. Similar to `._spectrum` above, almost all the configuration of `lfo` lives inside a json object, starting with `{` and ending with `}`.
All the parameters (except `id`) are written as `key: value` inside and separated by `,`. All the parameters (except `id`) are written as `key: value` inside and separated by `,`.
The reference refers to them as `config.key`, i.e. for the following one as `config.control` but you use them like below. The reference refers to them as `config.key`, i.e. for the following one as `config.control` but you use them like below.
+1 -4
View File
@@ -287,10 +287,7 @@ The modes are:
- `below`: the top note of the voicing is lower than or equal to the anchor (default) - `below`: the top note of the voicing is lower than or equal to the anchor (default)
- `above`: the bottom note of the voicing is higher than or equal to the anchor - `above`: the bottom note of the voicing is higher than or equal to the anchor
- `duck`: the top note of the voicing is lower than the anchor - `duck`: the top note of the voicing is lower than the anchor
- `root`: the bottom note of the voicing is always the lowest root note higher than or equal to the anchor - `root`: the bottom note of the voicing is always the root note closest to the anchor
- `oldabove` : old (buggy) behavior of above, kept for legacy reasons
- `oldroot` : old (buggy) behavior of root, kept for legacy reasons
The `anchor` can also be set from within the `mode` function: The `anchor` can also be set from within the `mode` function:
@@ -214,12 +214,7 @@ export const Reference = memo(function Reference() {
</p> </p>
)} )}
<p <p dangerouslySetInnerHTML={{ __html: entry.description }}></p>
dangerouslySetInnerHTML={{
// If we have an external url make sure it opens in another tab to not interrupt the player
__html: entry.description.replaceAll(/<a href="https:\/\//g, `<a target="_blank" href="https://`),
}}
></p>
<ul> <ul>
{entry.params?.map(({ name, type, description }, i) => ( {entry.params?.map(({ name, type, description }, i) => (
<li key={i}> <li key={i}>