mirror of
https://codeberg.org/uzu/strudel
synced 2026-08-15 09:38:28 -04:00
Compare commits
1 Commits
main
..
midi-timing
| Author | SHA1 | Date | |
|---|---|---|---|
| f1a541caec |
@@ -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()
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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] ]",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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 ]",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
Reference in New Issue
Block a user