Merge branch 'main' into patterns-tab

This commit is contained in:
Felix Roos 2023-09-17 17:28:18 +02:00
commit 6c3173f45b
76 changed files with 2981 additions and 531 deletions

View File

@ -42,7 +42,7 @@ jobs:
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libasound2-dev
- name: Install app dependencies from lockfile and build web
run: pnpm install

View File

@ -2,6 +2,7 @@
export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/desktopbridge/index.mjs';
export * from './packages/midi/index.mjs';
export * from './packages/mini/index.mjs';
export * from './packages/osc/index.mjs';

View File

@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "0.8.4",
"version": "0.9.0",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {

View File

@ -118,7 +118,10 @@ const generic_params = [
* @name fmh
* @param {number | Pattern} harmonicity
* @example
* note("c e g b").fm(4).fmh("<1 2 1.5 1.61>")
* note("c e g b")
* .fm(4)
* .fmh("<1 2 1.5 1.61>")
* .scope()
*
*/
[['fmh', 'fmi'], 'fmh'],
@ -130,10 +133,72 @@ const generic_params = [
* @param {number | Pattern} brightness modulation index
* @synonyms fmi
* @example
* note("c e g b").fm("<0 1 2 8 32>")
* note("c e g b")
* .fm("<0 1 2 8 32>")
* .scope()
*
*/
[['fmi', 'fmh'], 'fm'],
// fm envelope
/**
* Ramp type of fm envelope. Exp might be a bit broken..
*
* @name fmenv
* @param {number | Pattern} type lin | exp
* @example
* note("c e g b")
* .fm(4)
* .fmdecay(.2)
* .fmsustain(0)
* .fmenv("<exp lin>")
* .scope()
*
*/
['fmenv'],
/**
* Attack time for the FM envelope: time it takes to reach maximum modulation
*
* @name fmattack
* @param {number | Pattern} time attack time
* @example
* note("c e g b")
* .fm(4)
* .fmattack("<0 .05 .1 .2>")
* .scope()
*
*/
['fmattack'],
/**
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
*
* @name fmdecay
* @param {number | Pattern} time decay time
* @example
* note("c e g b")
* .fm(4)
* .fmdecay("<.01 .05 .1 .2>")
* .fmsustain(.4)
* .scope()
*
*/
['fmdecay'],
/**
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
*
* @name fmsustain
* @param {number | Pattern} level sustain level
* @example
* note("c e g b")
* .fm(4)
* .fmdecay(.1)
* .fmsustain("<1 .75 .5 0>")
* .scope()
*
*/
['fmsustain'],
// these are not really useful... skipping for now
['fmrelease'],
['fmvelocity'],
/**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
@ -146,6 +211,9 @@ const generic_params = [
*/
['bank'],
['analyze'], // analyser node send amount 0 - 1 (used by scope)
['fft'], // fftSize of analyser
/**
* Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level.
* Note that the decay is only audible if the sustain value is lower than 1.
@ -231,16 +299,52 @@ const generic_params = [
*/
['end'],
/**
* Loops the sample (from `begin` to `end`) the specified number of times.
* Loops the sample.
* Note that the tempo of the loop is not synced with the cycle tempo.
* To change the loop region, use loopBegin / loopEnd.
*
* @name loop
* @param {number | Pattern} times How often the sample is looped
* @param {number | Pattern} on If 1, the sample is looped
* @example
* s("bd").loop("<1 2 3 4>").osc()
* s("casio").loop(1)
*
*/
['loop'],
/**
* Begin to loop at a specific point in the sample (inbetween `begin` and `end`).
* Note that the loop point must be inbetween `begin` and `end`, and before `loopEnd`!
* Note: Samples starting with wt_ will automatically loop! (wt = wavetable)
*
* @name loopBegin
* @param {number | Pattern} time between 0 and 1, where 1 is the length of the sample
* @synonyms loopb
* @example
* s("space").loop(1)
* .loopBegin("<0 .125 .25>").scope()
*/
['loopBegin', 'loopb'],
/**
*
* End the looping section at a specific point in the sample (inbetween `begin` and `end`).
* Note that the loop point must be inbetween `begin` and `end`, and after `loopBegin`!
*
* @name loopEnd
* @param {number | Pattern} time between 0 and 1, where 1 is the length of the sample
* @synonyms loope
* @example
* s("space").loop(1)
* .loopEnd("<1 .75 .5 .25>").scope()
*/
['loopEnd', 'loope'],
/**
* bit crusher effect.
*
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
* @example
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
*
*/
// TODO: currently duplicated with "native" legato
// TODO: superdirt legato will do more: https://youtu.be/dQPmE1WaD1k?t=419
/**
@ -255,15 +359,6 @@ const generic_params = [
*/
// ['legato'],
// ['clhatdecay'],
/**
* bit crusher effect.
*
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
* @example
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
*
*/
['crush'],
/**
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
@ -275,7 +370,6 @@ const generic_params = [
*
*/
['coarse'],
/**
* choose the channel the pattern is sent to in superdirt
*
@ -309,6 +403,227 @@ const generic_params = [
*
*/
[['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'],
/**
* Sets the lowpass filter envelope modulation depth.
* @name lpenv
* @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_
* @synonyms lpe
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpa(.5)
* .lpenv("<4 2 1 0 -1 -2 -4>/4")
*/
['lpenv', 'lpe'],
/**
* Sets the highpass filter envelope modulation depth.
* @name hpenv
* @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_
* @synonyms hpe
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpa(.5)
* .hpenv("<4 2 1 0 -1 -2 -4>/4")
*/
['hpenv', 'hpe'],
/**
* Sets the bandpass filter envelope modulation depth.
* @name bpenv
* @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_
* @synonyms bpe
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpa(.5)
* .bpenv("<4 2 1 0 -1 -2 -4>/4")
*/
['bpenv', 'bpe'],
/**
* Sets the attack duration for the lowpass filter envelope.
* @name lpattack
* @param {number | Pattern} attack time of the filter envelope
* @synonyms lpa
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpa("<.5 .25 .1 .01>/4")
* .lpenv(4)
*/
['lpattack', 'lpa'],
/**
* Sets the attack duration for the highpass filter envelope.
* @name hpattack
* @param {number | Pattern} attack time of the highpass filter envelope
* @synonyms hpa
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpa("<.5 .25 .1 .01>/4")
* .hpenv(4)
*/
['hpattack', 'hpa'],
/**
* Sets the attack duration for the bandpass filter envelope.
* @name bpattack
* @param {number | Pattern} attack time of the bandpass filter envelope
* @synonyms bpa
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpa("<.5 .25 .1 .01>/4")
* .bpenv(4)
*/
['bpattack', 'bpa'],
/**
* Sets the decay duration for the lowpass filter envelope.
* @name lpdecay
* @param {number | Pattern} decay time of the filter envelope
* @synonyms lpd
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpd("<.5 .25 .1 0>/4")
* .lps(0.2)
* .lpenv(4)
*/
['lpdecay', 'lpd'],
/**
* Sets the decay duration for the highpass filter envelope.
* @name hpdecay
* @param {number | Pattern} decay time of the highpass filter envelope
* @synonyms hpd
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpd("<.5 .25 .1 0>/4")
* .hps(0.2)
* .hpenv(4)
*/
['hpdecay', 'hpd'],
/**
* Sets the decay duration for the bandpass filter envelope.
* @name bpdecay
* @param {number | Pattern} decay time of the bandpass filter envelope
* @synonyms bpd
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpd("<.5 .25 .1 0>/4")
* .bps(0.2)
* .bpenv(4)
*/
['bpdecay', 'bpd'],
/**
* Sets the sustain amplitude for the lowpass filter envelope.
* @name lpsustain
* @param {number | Pattern} sustain amplitude of the lowpass filter envelope
* @synonyms lps
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpd(.5)
* .lps("<0 .25 .5 1>/4")
* .lpenv(4)
*/
['lpsustain', 'lps'],
/**
* Sets the sustain amplitude for the highpass filter envelope.
* @name hpsustain
* @param {number | Pattern} sustain amplitude of the highpass filter envelope
* @synonyms hps
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpd(.5)
* .hps("<0 .25 .5 1>/4")
* .hpenv(4)
*/
['hpsustain', 'hps'],
/**
* Sets the sustain amplitude for the bandpass filter envelope.
* @name bpsustain
* @param {number | Pattern} sustain amplitude of the bandpass filter envelope
* @synonyms bps
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpd(.5)
* .bps("<0 .25 .5 1>/4")
* .bpenv(4)
*/
['bpsustain', 'bps'],
/**
* Sets the release time for the lowpass filter envelope.
* @name lprelease
* @param {number | Pattern} release time of the filter envelope
* @synonyms lpr
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .clip(.5)
* .lpf(500)
* .lpenv(4)
* .lpr("<.5 .25 .1 0>/4")
* .release(.5)
*/
['lprelease', 'lpr'],
/**
* Sets the release time for the highpass filter envelope.
* @name hprelease
* @param {number | Pattern} release time of the highpass filter envelope
* @synonyms hpr
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .clip(.5)
* .hpf(500)
* .hpenv(4)
* .hpr("<.5 .25 .1 0>/4")
* .release(.5)
*/
['hprelease', 'hpr'],
/**
* Sets the release time for the bandpass filter envelope.
* @name bprelease
* @param {number | Pattern} release time of the bandpass filter envelope
* @synonyms bpr
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .clip(.5)
* .bpf(500)
* .bpenv(4)
* .bpr("<.5 .25 .1 0>/4")
* .release(.5)
*/
['bprelease', 'bpr'],
/**
* Sets the filter type. The 24db filter is more aggressive. More types might be added in the future.
* @name ftype
* @param {number | Pattern} type 12db (default) or 24db
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .bpenv(4)
* .ftype("<12db 24db>")
*/
['ftype'],
['fanchor'],
/**
* Applies the cutoff frequency of the **h**igh-**p**ass **f**ilter.
*
@ -325,6 +640,36 @@ const generic_params = [
*/
// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496
// ['hpf'],
/**
* Applies a vibrato to the frequency of the oscillator.
*
* @name vib
* @synonyms vibrato, v
* @param {number | Pattern} frequency of the vibrato in hertz
* @example
* note("a")
* .vib("<.5 1 2 4 8 16>")
* @example
* // change the modulation depth with ":"
* note("a")
* .vib("<.5 1 2 4 8 16>:12")
*/
[['vib', 'vibmod'], 'vibrato', 'v'],
/**
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
*
* @name vibmod
* @synonyms vmod
* @param {number | Pattern} depth of vibrato (in semitones)
* @example
* note("a").vib(4)
* .vibmod("<.25 .5 1 2 12>")
* @example
* // change the vibrato frequency with ":"
* note("a")
* .vibmod("<.25 .5 1 2 12>:8")
*/
[['vibmod', 'vib'], 'vmod'],
[['hcutoff', 'hresonance'], 'hpf', 'hp'],
/**
* Controls the **h**igh-**p**ass **q**-value.
@ -503,6 +848,9 @@ const generic_params = [
*
*/
['lsize'],
// label for pianoroll
['activeLabel'],
[['label', 'activeLabel']],
// ['lfo'],
// ['lfocutoffint'],
// ['lfodelay'],
@ -796,8 +1144,22 @@ const generic_params = [
*
*/
['clip'],
];
// ZZFX
['zrand'],
['curve'],
['slide'], // superdirt duplicate
['deltaSlide'],
['pitchJump'],
['pitchJumpTime'],
['lfo', 'repeatTime'],
['noise'],
['zmod'],
['zcrush'], // like crush but scaled differently
['zdelay'],
['tremolo'],
['zzfx'],
];
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
controls.createParam = function (names) {

View File

@ -9,25 +9,29 @@ import { Pattern, getTime, State, TimeSpan } from './index.mjs';
export const getDrawContext = (id = 'test-canvas') => {
let canvas = document.querySelector('#' + id);
if (!canvas) {
const scale = 2; // 2 = crisp on retina screens
canvas = document.createElement('canvas');
canvas.id = id;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
document.body.prepend(canvas);
let timeout;
window.addEventListener('resize', () => {
timeout && clearTimeout(timeout);
timeout = setTimeout(() => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
}, 200);
});
}
return canvas.getContext('2d');
};
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
if (typeof window === 'undefined') {
return this;
}
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
@ -59,7 +63,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
export const cleanupDraw = (clearScreen = true) => {
const ctx = getDrawContext();
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.8.2",
"version": "0.9.0",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",

View File

@ -2273,14 +2273,14 @@ export const slice = register(
false, // turns off auto-patternification
);
/*
/**
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
* @name splice
* @memberof Pattern
* @returns Pattern
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
* s("breaks165")
* .splice(8, "0 1 [2 3 0]@2 3 0@2 7")
* .hurry(0.65)
*/
export const splice = register(
@ -2307,9 +2307,16 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto
return _loopAt(factor, pat, 1);
});
// this function will be redefined in repl.mjs to use the correct cps value.
// the fit function will be redefined in repl.mjs to use the correct cps value.
// It is still here to work in cases where repl.mjs is not used
/**
* Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
* Similar to loopAt.
* @name fit
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes/4").fit()
*/
export const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({

View File

@ -29,138 +29,26 @@ const getValue = (e) => {
return value;
};
Pattern.prototype.pianoroll = function ({
cycles = 4,
playhead = 0.5,
overscan = 1,
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 0,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
labels = 0,
} = {}) {
const ctx = getDrawContext();
const w = ctx.canvas.width;
const h = ctx.canvas.height;
Pattern.prototype.pianoroll = function (options = {}) {
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
to = timeframeProp;
}
const timeAxis = vertical ? h : w;
const valueAxis = vertical ? w : h;
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
let foldValues = [];
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
this.draw(
(ctx, events, t) => {
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
(ctx, haps, t) => {
const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
events.filter(inFrame).forEach((event) => {
const isActive = event.whole.begin <= t && event.endClipped > t;
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(t / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
pianoroll({
...options,
time: t,
ctx,
haps: haps.filter(inFrame),
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
ctx.strokeStyle = playheadColor;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
},
{
from: from - overscan,
to: to + overscan,
onQuery: (events) => {
const { min, max, values } = events.reduce(
({ min, max, values }, e) => {
const v = getValue(e);
return {
min: v < min ? v : min,
max: v > max ? v : max,
values: values.includes(v) ? values : [...values, v],
};
},
{ min: Infinity, max: -Infinity, values: [] },
);
if (autorange) {
minMidi = min;
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
},
},
);
return this;
@ -191,6 +79,13 @@ export function pianoroll({
fold = 0,
vertical = 0,
labels = false,
fill = 1,
fillActive = false,
strokeActive = true,
stroke,
hideInactive = 0,
colorizeInactive = 1,
fontFamily,
ctx,
} = {}) {
const w = ctx.canvas.width;
@ -234,58 +129,75 @@ export function pianoroll({
// foldValues = values.sort((a, b) => a - b);
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
/* const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
haps
// .filter(inFrame)
.forEach((event) => {
const isActive = event.whole.begin <= time && event.endClipped > time;
const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(time / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
haps.forEach((event) => {
const isActive = event.whole.begin <= time && event.endClipped > time;
let strokeCurrent = stroke ?? (strokeActive && isActive);
let fillCurrent = (!isActive && fill) || (isActive && fillActive);
if (hideInactive && !isActive) {
return;
}
let color = event.value?.color || event.context?.color;
active = color || active;
inactive = colorizeInactive ? color || inactive : inactive;
color = isActive ? active : inactive;
ctx.fillStyle = fillCurrent ? color : 'transparent';
ctx.strokeStyle = color;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
const timePx = scale(timeProgress, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valueProgress = fold
? foldValues.indexOf(value) / foldValues.length
: (Number(value) - minMidi) / valueExtent;
const valuePx = scale(valueProgress, ...valueRange);
let margin = 0;
const offset = scale(time / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
/* const xFactor = Math.sin(performance.now() / 500) + 1;
coords[0] *= xFactor; */
if (strokeCurrent) {
ctx.strokeRect(...coords);
}
if (fillCurrent) {
ctx.fillRect(...coords);
}
//ctx.ellipse(...ellipseFromRect(...coords))
if (labels) {
const defaultLabel = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
const { label: inactiveLabel, activeLabel } = event.value;
const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel;
const label = customLabel ?? defaultLabel;
let measure = vertical ? durationPx : barThickness * 0.75;
ctx.font = `${measure}px ${fontFamily || 'monospace'}`;
// font color
ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
@ -311,11 +223,15 @@ export function getDrawOptions(drawTime, options = {}) {
}
Pattern.prototype.punchcard = function (options) {
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }),
);
};
Pattern.prototype.wordfall = function (options) {
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
};
/* Pattern.prototype.pianoroll = function (options) {
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/csound",
"version": "0.8.0",
"version": "0.9.0",
"description": "csound bindings for strudel",
"main": "index.mjs",
"publishConfig": {

View File

@ -0,0 +1,3 @@
# @strudel/desktopbridge
This package contains utilities used to communicate with the Tauri backend

View File

@ -0,0 +1,9 @@
/*
index.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/desktopbridge/index.mjs>
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/>.
*/
export * from './midibridge.mjs';
export * from './utils.mjs';
export * from './oscbridge.mjs';

View File

@ -0,0 +1,11 @@
import { listen } from '@tauri-apps/api/event';
import { logger } from '../core/logger.mjs';
// listen for log events from the Tauri backend and log in the UI
await listen('log-event', (e) => {
if (e.payload == null) {
return;
}
const { message, message_type } = e.payload;
logger(message, message_type);
});

View File

@ -0,0 +1,52 @@
import { Invoke } from './utils.mjs';
import { Pattern, noteToMidi } from '@strudel.cycles/core';
const ON_MESSAGE = 0x90;
const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) {
return this.onTrigger((time, hap, currentTime) => {
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
const offset = (time - currentTime) * 1000;
const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
const roundedOffset = Math.round(offset);
const midichan = (hap.value.midichan ?? 1) - 1;
const requestedport = output ?? 'IAC';
const messagesfromjs = [];
if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
messagesfromjs.push({
requestedport,
message: [ON_MESSAGE + midichan, midiNumber, velocity],
offset: roundedOffset,
});
messagesfromjs.push({
requestedport,
message: [OFF_MESSAGE + midichan, midiNumber, velocity],
offset: roundedOffset + duration,
});
}
if (ccv && ccn) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
if (!['string', 'number'].includes(typeof ccn)) {
throw new Error('expected ccn to be a number or a string');
}
const scaled = Math.round(ccv * 127);
messagesfromjs.push({
requestedport,
message: [CC_MESSAGE + midichan, ccn, scaled],
offset: roundedOffset,
});
}
// invoke is temporarily blocking, run in an async process
if (messagesfromjs.length) {
setTimeout(() => {
Invoke('sendmidi', { messagesfromjs });
});
}
});
};

View File

@ -0,0 +1,43 @@
import { parseNumeral, Pattern } from '@strudel.cycles/core';
import { Invoke } from './utils.mjs';
Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
hap.ensureObjectValue();
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
// make sure n and note are numbers
controls.n && (controls.n = parseNumeral(controls.n));
controls.note && (controls.note = parseNumeral(controls.note));
const params = [];
const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);
Object.keys(controls).forEach((key) => {
const val = controls[key];
const value = typeof val === 'number' ? val.toString() : val;
if (value == null) {
return;
}
params.push({
name: key,
value,
valueisnumber: typeof val === 'number',
});
});
const messagesfromjs = [];
if (params.length) {
messagesfromjs.push({ target: '/dirt/play', timestamp, params });
}
if (messagesfromjs.length) {
setTimeout(() => {
Invoke('sendosc', { messagesfromjs });
});
}
});
};

View File

@ -0,0 +1,29 @@
{
"name": "@strudel/desktopbridge",
"version": "0.1.0",
"private": true,
"description": "tools/shims for communicating between the JS and Tauri (Rust) sides of the Studel desktop app",
"main": "index.mjs",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Jade Rowland <jaderowlanddev@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@tauri-apps/api": "^1.4.0"
},
"homepage": "https://github.com/tidalcycles/strudel#readme"
}

View File

@ -0,0 +1,4 @@
import { invoke } from '@tauri-apps/api/tauri';
export const Invoke = invoke;
export const isTauri = () => window.__TAURI_IPC__ != null;

View File

@ -78,7 +78,6 @@ function getDevice(output, outputs) {
return IACOutput ?? outputs[0];
}
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.midi = function (output) {
if (isPattern(output)) {
throw new Error(

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.8.0",
"version": "0.9.0",
"description": "Midi API for strudel",
"main": "index.mjs",
"publishConfig": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.8.2",
"version": "0.9.0",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.8.0",
"version": "0.9.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"publishConfig": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.8.0",
"version": "0.9.0",
"description": "React components for strudel",
"main": "src/index.js",
"publishConfig": {

View File

@ -25,10 +25,10 @@ function usePatternFrame({ pattern, started, getTime, onDraw, drawTime = [-2, 2]
const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase);
lastFrame.current = phase;
visibleHaps.current = (visibleHaps.current || [])
.filter((h) => h.whole.end >= phase - lookbehind - lookahead) // in frame
.filter((h) => h.endClipped >= phase - lookbehind - lookahead) // in frame
.concat(haps.filter((h) => h.hasOnset()));
onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime);
}, [pattern]),
}, [pattern, onDraw]),
);
useEffect(() => {
if (started) {

View File

@ -18,6 +18,7 @@ function useStrudel({
canvasId,
drawContext,
drawTime = [-2, 2],
paintOptions = {},
}) {
const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`;
@ -88,9 +89,9 @@ function useStrudel({
(pattern, time, haps, drawTime) => {
const { onPaint } = pattern.context || {};
const ctx = typeof drawContext === 'function' ? drawContext(canvasId) : drawContext;
onPaint?.(ctx, time, haps, drawTime);
onPaint?.(ctx, time, haps, drawTime, paintOptions);
},
[drawContext, canvasId],
[drawContext, canvasId, paintOptions],
);
const drawFirstFrame = useCallback(

View File

@ -8,4 +8,5 @@ export { default as usePostMessage } from './hooks/usePostMessage';
export { default as useKeydown } from './hooks/useKeydown';
export { default as useEvent } from './hooks/useEvent';
export { default as strudelTheme } from './themes/strudel-theme';
export { default as teletext } from './themes/teletext';
export { default as cx } from './cx';

View File

@ -12,6 +12,7 @@ export const settings = {
gutterBackground: 'transparent',
gutterForeground: '#0f380f',
light: true,
customStyle: '.cm-line { line-height: 1 }',
};
export default createTheme({
theme: 'light',
@ -35,5 +36,6 @@ export default createTheme({
{ tag: t.propertyName, color: '#0f380f' },
{ tag: t.className, color: '#0f380f' },
{ tag: t.invalid, color: '#0f380f' },
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
],
});

View File

@ -33,5 +33,6 @@ export default createTheme({
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});

View File

@ -36,5 +36,6 @@ export default createTheme({
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});

View File

@ -40,5 +40,6 @@ export default createTheme({
{ tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: '#ffffff' },
{ tag: [t.unit, t.punctuation], color: '#82aaff' },
],
});

View File

@ -0,0 +1,50 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
let colorA = '#6edee4';
//let colorB = 'magenta';
let colorB = 'white';
let colorC = 'red';
let colorD = '#f8fc55';
export const settings = {
background: '#000000',
foreground: colorA, // whats that?
caret: colorC,
selection: colorD,
selectionMatch: colorA,
lineHighlight: '#6edee440', // panel bg
lineBackground: '#00000040',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
customStyle: '.cm-line { line-height: 1 }',
};
let punctuation = colorD;
let mini = colorB;
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA },
{ tag: t.typeName, color: colorA },
{ tag: t.atom, color: colorA },
{ tag: t.number, color: mini },
{ tag: t.definition(t.variableName), color: colorA },
{ tag: t.string, color: mini },
{ tag: t.special(t.string), color: mini },
{ tag: t.comment, color: punctuation },
{ tag: t.variableName, color: colorA },
{ tag: t.tagName, color: colorA },
{ tag: t.bracket, color: punctuation },
{ tag: t.meta, color: colorA },
{ tag: t.attributeName, color: colorA },
{ tag: t.propertyName, color: colorA }, // methods
{ tag: t.className, color: colorA },
{ tag: t.invalid, color: colorC },
{ tag: [t.unit, t.punctuation], color: punctuation },
],
});

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.8.0",
"version": "0.9.0",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"publishConfig": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.8.2",
"version": "0.9.0",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {

View File

@ -161,5 +161,6 @@ Then just make sure your first call of `superdough` happens after a click of som
## Credits
- [ZZFX](https://github.com/KilledByAPixel/ZzFX) used for synths starting with z
- [SuperDirt](https://github.com/musikinformatik/SuperDirt)
- [WebDirt](https://github.com/dktr0/WebDirt)

View File

@ -1,4 +1,5 @@
import { getAudioContext } from './superdough.mjs';
import { clamp } from './util.mjs';
export function gainNode(value) {
const node = getAudioContext().createGain();
@ -6,17 +7,6 @@ export function gainNode(value) {
return node;
}
export const getOscillator = ({ s, freq, t }) => {
// make oscillator
const o = getAudioContext().createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.start(t);
//o.stop(t + duration + release);
const stop = (time) => o.stop(time);
return { node: o, stop };
};
// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future
export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => {
const gainNode = getAudioContext().createGain();
@ -39,6 +29,22 @@ export const getEnvelope = (attack, decay, sustain, release, velocity, begin) =>
};
};
export const getExpEnvelope = (attack, decay, sustain, release, velocity, begin) => {
sustain = Math.max(0.001, sustain);
velocity = Math.max(0.001, velocity);
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0.0001, begin);
gainNode.gain.exponentialRampToValueAtTime(velocity, begin + attack);
gainNode.gain.exponentialRampToValueAtTime(sustain * velocity, begin + attack + decay);
return {
node: gainNode,
stop: (t) => {
// similar to getEnvelope, this will glitch if sustain level has not been reached
gainNode.gain.exponentialRampToValueAtTime(0.0001, t + release);
},
};
};
export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
@ -61,10 +67,48 @@ export const getADSR = (attack, decay, sustain, release, velocity, begin, end) =
return gainNode;
};
export const getFilter = (type, frequency, Q) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
filter.Q.value = Q;
return filter;
export const getParamADSR = (param, attack, decay, sustain, release, min, max, begin, end) => {
const range = max - min;
const peak = min + range;
const sustainLevel = min + sustain * range;
param.setValueAtTime(min, begin);
param.linearRampToValueAtTime(peak, begin + attack);
param.linearRampToValueAtTime(sustainLevel, begin + attack + decay);
param.setValueAtTime(sustainLevel, end);
param.linearRampToValueAtTime(min, end + Math.max(release, 0.1));
};
export function createFilter(
context,
type,
frequency,
Q,
attack,
decay,
sustain,
release,
fenv,
start,
end,
fanchor = 0.5,
) {
const filter = context.createBiquadFilter();
filter.type = type;
filter.Q.value = Q;
filter.frequency.value = frequency;
// Apply ADSR to filter frequency
if (!isNaN(fenv) && fenv !== 0) {
const offset = fenv * fanchor;
const min = clamp(2 ** -offset * frequency, 0, 20000);
const max = clamp(2 ** (fenv - offset) * frequency, 0, 20000);
// console.log('min', min, 'max', max);
getParamADSR(filter.frequency, attack, decay, sustain, release, min, max, start, end);
return filter;
}
return filter;
}

View File

@ -8,4 +8,5 @@ export * from './superdough.mjs';
export * from './sampler.mjs';
export * from './helpers.mjs';
export * from './synth.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';

View File

@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "0.9.5",
"version": "0.9.8",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",

View File

@ -197,7 +197,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
const cutGroups = [];
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const {
let {
s,
freq,
unit,
@ -208,7 +208,9 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
n = 0,
note,
speed = 1, // sample playback speed
loopBegin = 0,
begin = 0,
loopEnd = 1,
end = 1,
} = value;
// load sample
@ -216,6 +218,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
// no playback
return;
}
loop = s.startsWith('wt_') ? 1 : value.loop;
const ac = getAudioContext();
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
@ -243,19 +246,12 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
bufferSource.start(time, offset);
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
/*if (loop) {
// TODO: idea for loopBegin / loopEnd
// if one of [loopBegin,loopEnd] is <= 1, interpret it as normlized
// if [loopBegin,loopEnd] is bigger >= 1, interpret it as sample number
// this will simplify perfectly looping things, while still keeping the normalized option
// the only drawback is that looping between samples 0 and 1 is not possible (which is not real use case)
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = offset;
bufferSource.loopEnd = offset + duration;
duration = loop * duration;
}*/
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
}
bufferSource.start(time, offset);
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
bufferSource.connect(envelope);
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
@ -266,9 +262,10 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
out.disconnect();
onended();
};
const stop = (endTime, playWholeBuffer = clip === undefined) => {
const stop = (endTime, playWholeBuffer = clip === undefined && loop === undefined) => {
let releaseTime = endTime;
if (playWholeBuffer) {
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
releaseTime = t + (end - begin) * bufferDuration;
}
bufferSource.stop(releaseTime + release);

View File

@ -9,7 +9,7 @@ import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback } from './util.mjs';
import workletsUrl from './worklets.mjs?url';
import { getFilter, gainNode } from './helpers.mjs';
import { createFilter, gainNode } from './helpers.mjs';
import { map } from 'nanostores';
import { logger } from './logger.mjs';
@ -121,6 +121,36 @@ function getReverb(orbit, duration = 2) {
return reverbs[orbit];
}
export let analyser, analyserData /* s = {} */;
export function getAnalyser(/* orbit, */ fftSize = 2048) {
if (!analyser /*s [orbit] */) {
const analyserNode = getAudioContext().createAnalyser();
analyserNode.fftSize = fftSize;
// getDestination().connect(analyserNode);
analyser /* s[orbit] */ = analyserNode;
//analyserData = new Uint8Array(analyser.frequencyBinCount);
analyserData = new Float32Array(analyser.frequencyBinCount);
}
if (analyser /* s[orbit] */.fftSize !== fftSize) {
analyser /* s[orbit] */.fftSize = fftSize;
//analyserData = new Uint8Array(analyser.frequencyBinCount);
analyserData = new Float32Array(analyser.frequencyBinCount);
}
return analyser /* s[orbit] */;
}
export function getAnalyzerData(type = 'time') {
const getter = {
time: () => analyser?.getFloatTimeDomainData(analyserData),
frequency: () => analyser?.getFloatFrequencyData(analyserData),
}[type];
if (!getter) {
throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`);
}
getter();
return analyserData;
}
function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
@ -137,6 +167,8 @@ export const superdough = async (value, deadline, hapDuration) => {
);
}
// duration is passed as value too..
value.duration = hapDuration;
// calculate absolute time
let t = ac.currentTime + deadline;
// destructure
@ -145,14 +177,32 @@ export const superdough = async (value, deadline, hapDuration) => {
bank,
source,
gain = 0.8,
// filters
ftype = '12db',
fanchor = 0.5,
// low pass
cutoff,
lpenv,
lpattack = 0.01,
lpdecay = 0.01,
lpsustain = 1,
lprelease = 0.01,
resonance = 1,
// high pass
hpenv,
hcutoff,
hpattack = 0.01,
hpdecay = 0.01,
hpsustain = 1,
hprelease = 0.01,
hresonance = 1,
// band pass
bpenv,
bandf,
bpattack = 0.01,
bpdecay = 0.01,
bpsustain = 1,
bprelease = 0.01,
bandq = 1,
//
coarse,
@ -167,6 +217,8 @@ export const superdough = async (value, deadline, hapDuration) => {
room,
size = 2,
velocity = 1,
analyze, // analyser wet
fft = 8, // fftSize 0 - 10
} = value;
gain = nanFallback(gain, 1);
gain *= velocity; // legacy fix for velocity
@ -206,11 +258,76 @@ export const superdough = async (value, deadline, hapDuration) => {
// gain stage
chain.push(gainNode(gain));
// filters
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
vowel !== undefined && chain.push(ac.createVowelFilter(vowel));
if (cutoff !== undefined) {
let lp = () =>
createFilter(
ac,
'lowpass',
cutoff,
resonance,
lpattack,
lpdecay,
lpsustain,
lprelease,
lpenv,
t,
t + hapDuration,
fanchor,
);
chain.push(lp());
if (ftype === '24db') {
chain.push(lp());
}
}
if (hcutoff !== undefined) {
let hp = () =>
createFilter(
ac,
'highpass',
hcutoff,
hresonance,
hpattack,
hpdecay,
hpsustain,
hprelease,
hpenv,
t,
t + hapDuration,
fanchor,
);
chain.push(hp());
if (ftype === '24db') {
chain.push(hp());
}
}
if (bandf !== undefined) {
let bp = () =>
createFilter(
ac,
'bandpass',
bandf,
bandq,
bpattack,
bpdecay,
bpsustain,
bprelease,
bpenv,
t,
t + hapDuration,
fanchor,
);
chain.push(bp());
if (ftype === '24db') {
chain.push(bp());
}
}
if (vowel !== undefined) {
const vowelFilter = ac.createVowelFilter(vowel);
chain.push(vowelFilter);
}
// effects
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
@ -242,12 +359,19 @@ export const superdough = async (value, deadline, hapDuration) => {
reverbSend = effectSend(post, reverbNode, room);
}
// analyser
let analyserSend;
if (analyze) {
const analyserNode = getAnalyser(/* orbit, */ 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, analyze);
}
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
// toDisconnect = all the node that should be disconnected in onended callback
// this is crucial for performance
toDisconnect = chain.concat([delaySend, reverbSend]);
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
};
export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps);

View File

@ -1,6 +1,6 @@
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { getOscillator, gainNode, getEnvelope } from './helpers.mjs';
import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs';
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
@ -26,37 +26,75 @@ export function registerSynthSounds() {
wave,
(t, value, onended) => {
// destructure adsr here, because the default should be different for synths and samples
const {
let {
attack = 0.001,
decay = 0.05,
sustain = 0.6,
release = 0.01,
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'lin',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
vib = 0,
vibmod = 0.5,
} = value;
let { n, note, freq } = value;
// with synths, n and note are the same thing
n = note || n || 36;
if (typeof n === 'string') {
n = noteToMidi(n); // e.g. c3 => 48
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof n === 'number') {
freq = midiToFreq(n); // + 48);
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
// make oscillator
const { node: o, stop } = getOscillator({ t, s: wave, freq });
const { node: o, stop } = getOscillator({
t,
s: wave,
freq,
vib,
vibmod,
partials: n,
});
let stopFm;
// FM + FM envelope
let stopFm, fmEnvelope;
if (fmModulationIndex) {
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex);
modulator.connect(o.frequency);
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(o.frequency);
} else {
fmAttack = fmAttack ?? 0.001;
fmDecay = fmDecay ?? 0.001;
fmSustain = fmSustain ?? 1;
fmRelease = fmRelease ?? 0.001;
fmVelocity = fmVelocity ?? 1;
fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
if (fmEnvelopeType === 'exp') {
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
fmEnvelope.node.maxValue = fmModulationIndex * 2;
fmEnvelope.node.minValue = 0.00001;
}
modulator.connect(fmEnvelope.node);
fmEnvelope.node.connect(o.frequency);
}
stopFm = stop;
}
// turn down
const g = gainNode(0.3);
// envelope
// gain envelope
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
o.onended = () => {
o.disconnect();
g.disconnect();
@ -66,6 +104,7 @@ export function registerSynthSounds() {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
releaseEnvelope(releaseTime);
fmEnvelope?.stop(releaseTime);
let end = releaseTime + release;
stop(end);
stopFm?.(end);
@ -76,3 +115,67 @@ export function registerSynthSounds() {
);
});
}
export function waveformN(partials, type) {
const real = new Float32Array(partials + 1);
const imag = new Float32Array(partials + 1);
const ac = getAudioContext();
const osc = ac.createOscillator();
const amplitudes = {
sawtooth: (n) => 1 / n,
square: (n) => (n % 2 === 0 ? 0 : 1 / n),
triangle: (n) => (n % 2 === 0 ? 0 : 1 / (n * n)),
};
if (!amplitudes[type]) {
throw new Error(`unknown wave type ${type}`);
}
real[0] = 0; // dc offset
imag[0] = 0;
let n = 1;
while (n <= partials) {
real[n] = amplitudes[type](n);
imag[n] = 0;
n++;
}
const wave = ac.createPeriodicWave(real, imag);
osc.setPeriodicWave(wave);
return osc;
}
export function getOscillator({ s, freq, t, vib, vibmod, partials }) {
// Make oscillator with partial count
let o;
if (!partials || s === 'sine') {
o = getAudioContext().createOscillator();
o.type = s || 'triangle';
} else {
o = waveformN(partials, s);
}
o.frequency.value = Number(freq);
o.start(t);
// Additional oscillator for vibrato effect
let vibrato_oscillator;
if (vib > 0) {
vibrato_oscillator = getAudioContext().createOscillator();
vibrato_oscillator.frequency.value = vib;
const gain = getAudioContext().createGain();
// Vibmod is the amount of vibrato, in semitones
gain.gain.value = vibmod * 100;
vibrato_oscillator.connect(gain);
gain.connect(o.detune);
vibrato_oscillator.start(t);
}
return {
node: o,
stop: (time) => {
vibrato_oscillator?.stop(time);
o.stop(time);
},
};
}

View File

@ -0,0 +1,124 @@
//import { ZZFX } from 'zzfx';
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { buildSamples } from './zzfx_fork.mjs';
export const getZZFX = (value, t) => {
let {
s,
note = 36,
freq,
//
zrand = 0,
attack = 0,
decay = 0,
sustain = 0.8,
release = 0.1,
curve = 1,
slide = 0,
deltaSlide = 0,
pitchJump = 0,
pitchJumpTime = 0,
lfo = 0,
noise = 0,
zmod = 0,
zcrush = 0,
zdelay = 0,
tremolo = 0,
duration = 0.2,
zzfx,
} = value;
const sustainTime = Math.max(duration - attack - decay, 0);
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note);
}
s = s.replace('z_', '');
const shape = ['sine', 'triangle', 'sawtooth', 'tan', 'noise'].indexOf(s) || 0;
curve = s === 'square' ? 0 : curve;
const params = zzfx || [
0.25, // volume
zrand,
freq,
attack,
sustainTime,
release,
shape,
curve,
slide,
deltaSlide,
pitchJump,
pitchJumpTime,
lfo,
noise,
zmod,
zcrush,
zdelay,
sustain, // sustain volume!
decay,
tremolo,
];
// console.log(redableZZFX(params));
const samples = /* ZZFX. */ buildSamples(...params);
const context = getAudioContext();
const buffer = context.createBuffer(1, samples.length, context.sampleRate);
buffer.getChannelData(0).set(samples);
const source = getAudioContext().createBufferSource();
source.buffer = buffer;
source.start(t);
return {
node: source,
};
};
export function registerZZFXSounds() {
['zzfx', 'z_sine', 'z_sawtooth', 'z_triangle', 'z_square', 'z_tan', 'z_noise'].forEach((wave) => {
registerSound(
wave,
(t, value, onended) => {
const { node: o } = getZZFX({ s: wave, ...value }, t);
o.onended = () => {
o.disconnect();
onended();
};
return {
node: o,
stop: () => {},
};
},
{ type: 'synth', prebake: true },
);
});
}
// just for debugging
function redableZZFX(params) {
const paramOrder = [
'volume',
'zrand',
'frequency',
'attack',
'sustain',
'release',
'shape',
'curve',
'slide',
'deltaSlide',
'pitchJump',
'pitchJumpTime',
'lfo',
'noise',
'zmod',
'zcrush',
'zdelay',
'sustainVolume',
'decay',
'tremolo',
];
return Object.fromEntries(paramOrder.map((param, i) => [param, params[i]]));
}

View File

@ -0,0 +1,120 @@
import { getAudioContext } from './superdough.mjs';
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
export function buildSamples(
volume = 1,
randomness = 0.05,
frequency = 220,
attack = 0,
sustain = 0,
release = 0.1,
shape = 0,
shapeCurve = 1,
slide = 0,
deltaSlide = 0,
pitchJump = 0,
pitchJumpTime = 0,
repeatTime = 0,
noise = 0,
modulation = 0,
bitCrush = 0,
delay = 0,
sustainVolume = 1,
decay = 0,
tremolo = 0,
) {
// init parameters
let PI2 = Math.PI * 2,
sampleRate = getAudioContext().sampleRate,
sign = (v) => (v > 0 ? 1 : -1),
startSlide = (slide *= (500 * PI2) / sampleRate / sampleRate),
startFrequency = (frequency *= ((1 + randomness * 2 * Math.random() - randomness) * PI2) / sampleRate),
b = [],
t = 0,
tm = 0,
i = 0,
j = 1,
r = 0,
c = 0,
s = 0,
f,
length;
// scale by sample rate
attack = attack * sampleRate + 9; // minimum attack to prevent pop
decay *= sampleRate;
sustain *= sampleRate;
release *= sampleRate;
delay *= sampleRate;
deltaSlide *= (500 * PI2) / sampleRate ** 3;
modulation *= PI2 / sampleRate;
pitchJump *= PI2 / sampleRate;
pitchJumpTime *= sampleRate;
repeatTime = (repeatTime * sampleRate) | 0;
// generate waveform
for (length = (attack + decay + sustain + release + delay) | 0; i < length; b[i++] = s) {
if (!(++c % ((bitCrush * 100) | 0))) {
// bit crush
s = shape
? shape > 1
? shape > 2
? shape > 3 // wave shape
? Math.sin((t % PI2) ** 3) // 4 noise
: Math.max(Math.min(Math.tan(t), 1), -1) // 3 tan
: 1 - (((((2 * t) / PI2) % 2) + 2) % 2) // 2 saw
: 1 - 4 * Math.abs(Math.round(t / PI2) - t / PI2) // 1 triangle
: Math.sin(t); // 0 sin
s =
(repeatTime
? 1 - tremolo + tremolo * Math.sin((PI2 * i) / repeatTime) // tremolo
: 1) *
sign(s) *
Math.abs(s) ** shapeCurve * // curve 0=square, 2=pointy
volume *
1 * // envelope
(i < attack
? i / attack // attack
: i < attack + decay // decay
? 1 - ((i - attack) / decay) * (1 - sustainVolume) // decay falloff
: i < attack + decay + sustain // sustain
? sustainVolume // sustain volume
: i < length - delay // release
? ((length - i - delay) / release) * // release falloff
sustainVolume // release volume
: 0); // post release
s = delay
? s / 2 +
(delay > i
? 0 // delay
: ((i < length - delay ? 1 : (length - i) / delay) * // release delay
b[(i - delay) | 0]) /
2)
: s; // sample delay
}
f =
(frequency += slide += deltaSlide) * // frequency
Math.cos(modulation * tm++); // modulation
t += f - f * noise * (1 - (((Math.sin(i) + 1) * 1e9) % 2)); // noise
if (j && ++j > pitchJumpTime) {
// pitch jump
frequency += pitchJump; // apply pitch jump
startFrequency += pitchJump; // also apply to start
j = 0; // stop pitch jump time
}
if (repeatTime && !(++r % repeatTime)) {
// repeat
frequency = startFrequency; // reset frequency
slide = startSlide; // reset slide
j ||= 1; // reset pitch jump time
}
}
return b;
}

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.8.2",
"version": "0.9.0",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/transpiler",
"version": "0.8.2",
"version": "0.9.0",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"publishConfig": {

View File

@ -1,6 +1,6 @@
{
"name": "@strudel/web",
"version": "0.8.3",
"version": "0.9.0",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"main": "web.mjs",
"publishConfig": {

View File

@ -5,4 +5,5 @@ This program is free software: you can redistribute it and/or modify it under th
*/
export * from './webaudio.mjs';
export * from './scope.mjs';
export * from 'superdough';

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.8.2",
"version": "0.9.0",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",

View File

@ -0,0 +1,88 @@
import { Pattern, getDrawContext, clamp } from '@strudel.cycles/core';
import { analyser, getAnalyzerData } from 'superdough';
export function drawTimeScope(
analyser,
{ align = true, color = 'white', thickness = 3, scale = 0.25, pos = 0.75, next = 1, trigger = 0 } = {},
) {
const ctx = getDrawContext();
const dataArray = getAnalyzerData('time');
ctx.lineWidth = thickness;
ctx.strokeStyle = color;
ctx.beginPath();
let canvas = ctx.canvas;
const bufferSize = analyser.frequencyBinCount;
let triggerIndex = align
? Array.from(dataArray).findIndex((v, i, arr) => i && arr[i - 1] > -trigger && v <= -trigger)
: 0;
triggerIndex = Math.max(triggerIndex, 0); // fallback to 0 when no trigger is found
const sliceWidth = (canvas.width * 1.0) / bufferSize;
let x = 0;
for (let i = triggerIndex; i < bufferSize; i++) {
const v = dataArray[i] + 1;
const y = (scale * (v - 1) + pos) * canvas.height;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
x += sliceWidth;
}
ctx.stroke();
}
export function drawFrequencyScope(
analyser,
{ color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0 } = {},
) {
const dataArray = getAnalyzerData('frequency');
const ctx = getDrawContext();
const canvas = ctx.canvas;
ctx.fillStyle = color;
const bufferSize = analyser.frequencyBinCount;
const sliceWidth = (canvas.width * 1.0) / bufferSize;
let x = 0;
for (let i = 0; i < bufferSize; i++) {
const normalized = clamp((dataArray[i] - min) / (max - min), 0, 1);
const v = normalized * scale;
const h = v * canvas.height;
const y = (pos - v * lean) * canvas.height;
ctx.fillRect(x, y, Math.max(sliceWidth, 1), h);
x += sliceWidth;
}
}
function clearScreen(smear = 0, smearRGB = `0,0,0`) {
const ctx = getDrawContext();
if (!smear) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
} else {
ctx.fillStyle = `rgba(${smearRGB},${1 - smear})`;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
}
Pattern.prototype.fscope = function (config = {}) {
return this.analyze(1).draw(() => {
clearScreen(config.smear);
analyser && drawFrequencyScope(analyser, config);
});
};
Pattern.prototype.tscope = function (config = {}) {
return this.analyze(1).draw(() => {
clearScreen(config.smear);
analyser && drawTimeScope(analyser, config);
});
};
Pattern.prototype.scope = Pattern.prototype.tscope;

View File

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/xen",
"version": "0.8.0",
"version": "0.9.0",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"publishConfig": {

21
pnpm-lock.yaml generated
View File

@ -176,6 +176,15 @@ importers:
specifier: ^4.3.3
version: 4.3.3(@types/node@18.16.3)
packages/desktopbridge:
dependencies:
'@strudel.cycles/core':
specifier: workspace:*
version: link:../core
'@tauri-apps/api':
specifier: ^1.4.0
version: 1.4.0
packages/embed: {}
packages/midi:
@ -579,6 +588,9 @@ importers:
'@strudel.cycles/xen':
specifier: workspace:*
version: link:../packages/xen
'@strudel/desktopbridge':
specifier: workspace:*
version: link:../packages/desktopbridge
'@supabase/supabase-js':
specifier: ^2.21.0
version: 2.21.0
@ -588,6 +600,9 @@ importers:
'@tailwindcss/typography':
specifier: ^0.5.8
version: 0.5.9(tailwindcss@3.3.2)
'@tauri-apps/api':
specifier: ^1.4.0
version: 1.4.0
'@types/node':
specifier: ^18.16.3
version: 18.16.3
@ -3944,6 +3959,11 @@ packages:
tailwindcss: 3.3.2
dev: false
/@tauri-apps/api@1.4.0:
resolution: {integrity: sha512-Jd6HPoTM1PZSFIzq7FB8VmMu3qSSyo/3lSwLpoapW+lQ41CL5Dow2KryLg+gyazA/58DRWI9vu/XpEeHK4uMdw==}
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
dev: false
/@tauri-apps/cli-darwin-arm64@1.4.0:
resolution: {integrity: sha512-nA/ml0SfUt6/CYLVbHmT500Y+ijqsuv5+s9EBnVXYSLVg9kbPUZJJHluEYK+xKuOj6xzyuT/+rZFMRapmJD3jQ==}
engines: {node: '>= 10'}
@ -8025,6 +8045,7 @@ packages:
/iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
safer-buffer: 2.1.2
dev: true

271
src-tauri/Cargo.lock generated
View File

@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
@ -41,6 +50,28 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "alsa"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47"
dependencies = [
"alsa-sys",
"bitflags",
"libc",
"nix",
]
[[package]]
name = "alsa-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@ -66,10 +97,13 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
name = "app"
version = "0.1.0"
dependencies = [
"midir",
"rosc",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tokio",
]
[[package]]
@ -102,6 +136,21 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.1"
@ -378,6 +427,26 @@ dependencies = [
"libc",
]
[[package]]
name = "coremidi"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7847ca018a67204508b77cb9e6de670125075f7464fff5f673023378fa34f5"
dependencies = [
"core-foundation",
"core-foundation-sys",
"coremidi-sys",
]
[[package]]
name = "coremidi-sys"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79a6deed0c97b2d40abbab77e4c97f81d71e162600423382c277dd640019116c"
dependencies = [
"core-foundation-sys",
]
[[package]]
name = "cpufeatures"
version = "0.2.8"
@ -910,6 +979,12 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "gimli"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "gio"
version = "0.15.12"
@ -1467,6 +1542,28 @@ dependencies = [
"autocfg",
]
[[package]]
name = "midir"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a456444d83e7ead06ae6a5c0a215ed70282947ff3897fb45fcb052b757284731"
dependencies = [
"alsa",
"bitflags",
"coremidi",
"js-sys",
"libc",
"wasm-bindgen",
"web-sys",
"windows 0.43.0",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.1"
@ -1477,6 +1574,17 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "mio"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
]
[[package]]
name = "ndk"
version = "0.6.0"
@ -1511,12 +1619,33 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nix"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
dependencies = [
"bitflags",
"cfg-if",
"libc",
]
[[package]]
name = "nodrop"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -1616,6 +1745,15 @@ dependencies = [
"objc",
]
[[package]]
name = "object"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.18.0"
@ -1782,9 +1920,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.9"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pin-utils"
@ -2052,6 +2190,22 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
[[package]]
name = "rosc"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2e63d9e6b0d090be1485cf159b1e04c3973d2d3e1614963544ea2ff47a4a981"
dependencies = [
"byteorder",
"nom",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -2274,6 +2428,15 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.5"
@ -2301,6 +2464,16 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "socket2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "soup2"
version = "0.2.1"
@ -2785,17 +2958,34 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.28.2"
version = "1.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2"
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
dependencies = [
"autocfg",
"backtrace",
"bytes",
"libc",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
]
[[package]]
name = "toml"
version = "0.5.11"
@ -3090,6 +3280,16 @@ version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]]
name = "web-sys"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webkit2gtk"
version = "0.18.2"
@ -3220,6 +3420,21 @@ dependencies = [
"windows_x86_64_msvc 0.39.0",
]
[[package]]
name = "windows"
version = "0.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows"
version = "0.48.0"
@ -3270,12 +3485,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_gnullvm 0.48.0",
"windows_aarch64_msvc 0.48.0",
"windows_i686_gnu 0.48.0",
"windows_i686_msvc 0.48.0",
"windows_x86_64_gnu 0.48.0",
"windows_x86_64_gnullvm",
"windows_x86_64_gnullvm 0.48.0",
"windows_x86_64_msvc 0.48.0",
]
@ -3285,6 +3500,12 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
@ -3297,6 +3518,12 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
@ -3309,6 +3536,12 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
@ -3321,6 +3554,12 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
@ -3333,12 +3572,24 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
@ -3351,6 +3602,12 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"

View File

@ -18,6 +18,9 @@ tauri-build = { version = "1.4.0", features = [] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.4.0", features = ["fs-all"] }
midir = "0.9.1"
tokio = { version = "1.29.0", features = ["full"] }
rosc = "0.10.1"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.

View File

@ -0,0 +1,20 @@
use std::sync::Arc;
use tauri::Window;
#[derive(Clone, serde::Serialize)]
pub struct LoggerPayload {
pub message: String,
pub message_type: String,
}
#[derive(Clone)]
pub struct Logger {
pub window: Arc<Window>,
}
impl Logger {
pub fn log(&self, message: String, message_type: String) {
println!("{}", message);
let _ = self.window.emit("log-event", LoggerPayload { message, message_type });
}
}

View File

@ -1,8 +1,47 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod midibridge;
mod oscbridge;
mod loggerbridge;
use std::sync::Arc;
use loggerbridge::Logger;
use tauri::Manager;
use tokio::sync::mpsc;
use tokio::sync::Mutex;
// the payload type must implement `Serialize` and `Clone`.
#[derive(Clone, serde::Serialize)]
struct Payload {
message: String,
message_type: String,
}
fn main() {
tauri::Builder::default()
let (async_input_transmitter_midi, async_input_receiver_midi) = mpsc::channel(1);
let (async_output_transmitter_midi, async_output_receiver_midi) = mpsc::channel(1);
let (async_input_transmitter_osc, async_input_receiver_osc) = mpsc::channel(1);
let (async_output_transmitter_osc, async_output_receiver_osc) = mpsc::channel(1);
tauri::Builder
::default()
.manage(midibridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_midi),
})
.manage(oscbridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_osc),
})
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc])
.setup(|app| {
let window = Arc::new(app.get_window("main").unwrap());
let logger = Logger { window };
midibridge::init(
logger.clone(),
async_input_receiver_midi,
async_output_receiver_midi,
async_output_transmitter_midi
);
oscbridge::init(logger, async_input_receiver_osc, async_output_receiver_osc, async_output_transmitter_osc);
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

158
src-tauri/src/midibridge.rs Normal file
View File

@ -0,0 +1,158 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use midir::MidiOutput;
use tokio::sync::{ mpsc, Mutex };
use tokio::time::Instant;
use serde::Deserialize;
use std::thread::sleep;
use crate::loggerbridge::Logger;
pub struct MidiMessage {
pub message: Vec<u8>,
pub instant: Instant,
pub offset: u64,
pub requestedport: String,
}
pub struct AsyncInputTransmit {
pub inner: Mutex<mpsc::Sender<Vec<MidiMessage>>>,
}
pub fn init(
logger: Logger,
async_input_receiver: mpsc::Receiver<Vec<MidiMessage>>,
mut async_output_receiver: mpsc::Receiver<Vec<MidiMessage>>,
async_output_transmitter: mpsc::Sender<Vec<MidiMessage>>
) {
tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
let message_queue: Arc<Mutex<Vec<MidiMessage>>> = Arc::new(Mutex::new(Vec::new()));
/* ...........................................................
Listen For incoming messages and add to queue
............................................................*/
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
loop {
if let Some(package) = async_output_receiver.recv().await {
let mut message_queue = message_queue_clone.lock().await;
let messages = package;
for message in messages {
(*message_queue).push(message);
}
}
}
});
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
/* ...........................................................
Open Midi Ports
............................................................*/
let midiout = MidiOutput::new("strudel").unwrap();
let out_ports = midiout.ports();
let mut port_names = Vec::new();
//TODO: Send these print messages to the UI logger instead of the rust console so the user can see them
if out_ports.len() == 0 {
logger.log(
" No MIDI devices found. Connect a device or enable IAC Driver to enable midi.".to_string(),
"".to_string()
);
// logger(window, " No MIDI devices found. Connect a device or enable IAC Driver.".to_string(), None);
return;
}
// give the frontend couple seconds to load on start, or the log messages will get lost
sleep(Duration::from_secs(3));
logger.log(format!("Found {} midi devices!", out_ports.len()), "".to_string());
// the user could reference any port at anytime during runtime,
// so let's go ahead and open them all (same behavior as web app)
let mut output_connections = HashMap::new();
for i in 0..=out_ports.len().saturating_sub(1) {
let midiout = MidiOutput::new("strudel").unwrap();
let ports = midiout.ports();
let port = ports.get(i).unwrap();
let port_name = midiout.port_name(port).unwrap();
logger.log(port_name.clone(), "".to_string());
let out_con = midiout.connect(port, &port_name).unwrap();
port_names.insert(i, port_name.clone());
output_connections.insert(port_name, out_con);
}
/* ...........................................................
Process queued messages
............................................................*/
loop {
let mut message_queue = message_queue_clone.lock().await;
//iterate over each message, play and remove messages when they are ready
message_queue.retain(|message| {
if message.instant.elapsed().as_millis() < message.offset.into() {
return true;
}
let mut out_con = output_connections.get_mut(&message.requestedport);
// WebMidi supports getting a connection by part of its name
// ex: 'bus 1' instead of 'IAC Driver bus 1' so let's emulate that behavior
if out_con.is_none() {
let key = port_names.iter().find(|port_name| {
return port_name.contains(&message.requestedport);
});
if key.is_some() {
out_con = output_connections.get_mut(key.unwrap());
}
}
if out_con.is_some() {
// process the message
if let Err(err) = (&mut out_con.unwrap()).send(&message.message) {
logger.log(format!("Midi message send error: {}", err), "error".to_string());
}
} else {
logger.log(format!("failed to find midi device: {}", message.requestedport), "error".to_string());
}
return false;
});
sleep(Duration::from_millis(1));
}
});
}
pub async fn async_process_model(
mut input_reciever: mpsc::Receiver<Vec<MidiMessage>>,
output_transmitter: mpsc::Sender<Vec<MidiMessage>>
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
while let Some(input) = input_reciever.recv().await {
let output = input;
output_transmitter.send(output).await?;
}
Ok(())
}
#[derive(Deserialize)]
pub struct MessageFromJS {
message: Vec<u8>,
offset: u64,
requestedport: String,
}
// Called from JS
#[tauri::command]
pub async fn sendmidi(
messagesfromjs: Vec<MessageFromJS>,
state: tauri::State<'_, AsyncInputTransmit>
) -> Result<(), String> {
let async_proc_input_tx = state.inner.lock().await;
let mut messages_to_process: Vec<MidiMessage> = Vec::new();
for m in messagesfromjs {
let message_to_process = MidiMessage {
instant: Instant::now(),
message: m.message,
offset: m.offset,
requestedport: m.requestedport,
};
messages_to_process.push(message_to_process);
}
async_proc_input_tx.send(messages_to_process).await.map_err(|e| e.to_string())
}

157
src-tauri/src/oscbridge.rs Normal file
View File

@ -0,0 +1,157 @@
use rosc::{ encoder, OscTime };
use rosc::{ OscMessage, OscPacket, OscType, OscBundle };
use std::net::UdpSocket;
use std::time::Duration;
use std::sync::Arc;
use tokio::sync::{ mpsc, Mutex };
use serde::Deserialize;
use std::thread::sleep;
use crate::loggerbridge::Logger;
pub struct OscMsg {
pub msg_buf: Vec<u8>,
pub timestamp: u64,
}
pub struct AsyncInputTransmit {
pub inner: Mutex<mpsc::Sender<Vec<OscMsg>>>,
}
const UNIX_OFFSET: u64 = 2_208_988_800; // 70 years in seconds
const TWO_POW_32: f64 = (u32::MAX as f64) + 1.0; // Number of bits in a `u32`
const NANOS_PER_SECOND: f64 = 1.0e9;
const SECONDS_PER_NANO: f64 = 1.0 / NANOS_PER_SECOND;
pub fn init(
logger: Logger,
async_input_receiver: mpsc::Receiver<Vec<OscMsg>>,
mut async_output_receiver: mpsc::Receiver<Vec<OscMsg>>,
async_output_transmitter: mpsc::Sender<Vec<OscMsg>>
) {
tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
let message_queue: Arc<Mutex<Vec<OscMsg>>> = Arc::new(Mutex::new(Vec::new()));
/* ...........................................................
Listen For incoming messages and add to queue
............................................................*/
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
loop {
if let Some(package) = async_output_receiver.recv().await {
let mut message_queue = message_queue_clone.lock().await;
let messages = package;
for message in messages {
(*message_queue).push(message);
}
}
}
});
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
/* ...........................................................
Open OSC Ports
............................................................*/
let sock = UdpSocket::bind("127.0.0.1:57122").unwrap();
let to_addr = String::from("127.0.0.1:57120");
sock.set_nonblocking(true).unwrap();
sock.connect(to_addr).expect("could not connect to OSC address");
/* ...........................................................
Process queued messages
............................................................*/
loop {
let mut message_queue = message_queue_clone.lock().await;
message_queue.retain(|message| {
let result = sock.send(&message.msg_buf);
if result.is_err() {
logger.log(
format!("OSC Message failed to send, the server might no longer be available"),
"error".to_string()
);
}
return false;
});
sleep(Duration::from_millis(1));
}
});
}
pub async fn async_process_model(
mut input_reciever: mpsc::Receiver<Vec<OscMsg>>,
output_transmitter: mpsc::Sender<Vec<OscMsg>>
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
while let Some(input) = input_reciever.recv().await {
let output = input;
output_transmitter.send(output).await?;
}
Ok(())
}
#[derive(Deserialize)]
pub struct Param {
name: String,
value: String,
valueisnumber: bool,
}
#[derive(Deserialize)]
pub struct MessageFromJS {
params: Vec<Param>,
timestamp: u64,
target: String,
}
// Called from JS
#[tauri::command]
pub async fn sendosc(
messagesfromjs: Vec<MessageFromJS>,
state: tauri::State<'_, AsyncInputTransmit>
) -> Result<(), String> {
let async_proc_input_tx = state.inner.lock().await;
let mut messages_to_process: Vec<OscMsg> = Vec::new();
for m in messagesfromjs {
let mut args = Vec::new();
for p in m.params {
args.push(OscType::String(p.name));
if p.valueisnumber {
args.push(OscType::Float(p.value.parse().unwrap()));
} else {
args.push(OscType::String(p.value));
}
}
let duration_since_epoch = Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0);
let seconds = u32
::try_from(duration_since_epoch.as_secs())
.map_err(|_| "bit conversion failed for osc message timetag")?;
let nanos = duration_since_epoch.subsec_nanos() as f64;
let fractional = (nanos * SECONDS_PER_NANO * TWO_POW_32).round() as u32;
let timetag = OscTime::from((seconds, fractional));
let packet = OscPacket::Message(OscMessage {
addr: m.target,
args,
});
let bundle = OscBundle {
content: vec![packet],
timetag,
};
let msg_buf = encoder::encode(&OscPacket::Bundle(bundle)).unwrap();
let message_to_process = OscMsg {
msg_buf,
timestamp: m.timestamp,
};
messages_to_process.push(message_to_process);
}
async_proc_input_tx.send(messages_to_process).await.map_err(|e| e.to_string())
}

View File

@ -900,6 +900,33 @@ exports[`runs examples > example "begin" example index 0 1`] = `
]
`;
exports[`runs examples > example "bpattack" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
]
`;
exports[`runs examples > example "bpdecay" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0.2 bpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0.2 bpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0.2 bpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0.2 bpenv:4 ]",
]
`;
exports[`runs examples > example "bpenv" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
]
`;
exports[`runs examples > example "bpf" example index 0 1`] = `
[
"[ 0/1 → 1/3 | s:hh bandf:1000 ]",
@ -938,6 +965,24 @@ exports[`runs examples > example "bpq" example index 0 1`] = `
]
`;
exports[`runs examples > example "bprelease" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth clip:0.5 bandf:500 bpenv:4 bprelease:0.5 release:0.5 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth clip:0.5 bandf:500 bpenv:4 bprelease:0.5 release:0.5 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth clip:0.5 bandf:500 bpenv:4 bprelease:0.5 release:0.5 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth clip:0.5 bandf:500 bpenv:4 bprelease:0.5 release:0.5 ]",
]
`;
exports[`runs examples > example "bpsustain" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0 bpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0 bpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0 bpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0 bpenv:4 ]",
]
`;
exports[`runs examples > example "cat" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:hh ]",
@ -1783,6 +1828,15 @@ exports[`runs examples > example "firstOf" example index 0 1`] = `
]
`;
exports[`runs examples > example "fit" example index 0 1`] = `
[
"[ (0/1 → 1/1) ⇝ 4/1 | s:rhodes speed:0.25 unit:c ]",
"[ 0/1 ⇜ (1/1 → 2/1) ⇝ 4/1 | s:rhodes speed:0.25 unit:c ]",
"[ 0/1 ⇜ (2/1 → 3/1) ⇝ 4/1 | s:rhodes speed:0.25 unit:c ]",
"[ 0/1 ⇜ (3/1 → 4/1) | s:rhodes speed:0.25 unit:c ]",
]
`;
exports[`runs examples > example "floor" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:42 ]",
@ -1806,43 +1860,127 @@ exports[`runs examples > example "floor" example index 0 1`] = `
exports[`runs examples > example "fm" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:0 ]",
"[ 1/4 → 1/2 | note:e fmi:0 ]",
"[ 1/2 → 3/4 | note:g fmi:0 ]",
"[ 3/4 → 1/1 | note:b fmi:0 ]",
"[ 1/1 → 5/4 | note:c fmi:1 ]",
"[ 5/4 → 3/2 | note:e fmi:1 ]",
"[ 3/2 → 7/4 | note:g fmi:1 ]",
"[ 7/4 → 2/1 | note:b fmi:1 ]",
"[ 2/1 → 9/4 | note:c fmi:2 ]",
"[ 9/4 → 5/2 | note:e fmi:2 ]",
"[ 5/2 → 11/4 | note:g fmi:2 ]",
"[ 11/4 → 3/1 | note:b fmi:2 ]",
"[ 3/1 → 13/4 | note:c fmi:8 ]",
"[ 13/4 → 7/2 | note:e fmi:8 ]",
"[ 7/2 → 15/4 | note:g fmi:8 ]",
"[ 15/4 → 4/1 | note:b fmi:8 ]",
"[ 0/1 → 1/4 | note:c fmi:0 analyze:1 ]",
"[ 1/4 → 1/2 | note:e fmi:0 analyze:1 ]",
"[ 1/2 → 3/4 | note:g fmi:0 analyze:1 ]",
"[ 3/4 → 1/1 | note:b fmi:0 analyze:1 ]",
"[ 1/1 → 5/4 | note:c fmi:1 analyze:1 ]",
"[ 5/4 → 3/2 | note:e fmi:1 analyze:1 ]",
"[ 3/2 → 7/4 | note:g fmi:1 analyze:1 ]",
"[ 7/4 → 2/1 | note:b fmi:1 analyze:1 ]",
"[ 2/1 → 9/4 | note:c fmi:2 analyze:1 ]",
"[ 9/4 → 5/2 | note:e fmi:2 analyze:1 ]",
"[ 5/2 → 11/4 | note:g fmi:2 analyze:1 ]",
"[ 11/4 → 3/1 | note:b fmi:2 analyze:1 ]",
"[ 3/1 → 13/4 | note:c fmi:8 analyze:1 ]",
"[ 13/4 → 7/2 | note:e fmi:8 analyze:1 ]",
"[ 7/2 → 15/4 | note:g fmi:8 analyze:1 ]",
"[ 15/4 → 4/1 | note:b fmi:8 analyze:1 ]",
]
`;
exports[`runs examples > example "fmattack" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmattack:0 analyze:1 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmattack:0 analyze:1 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmattack:0 analyze:1 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmattack:0 analyze:1 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmattack:0.05 analyze:1 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmattack:0.05 analyze:1 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmattack:0.05 analyze:1 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmattack:0.05 analyze:1 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmattack:0.1 analyze:1 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmattack:0.1 analyze:1 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmattack:0.1 analyze:1 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmattack:0.1 analyze:1 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmattack:0.2 analyze:1 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmattack:0.2 analyze:1 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmattack:0.2 analyze:1 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmattack:0.2 analyze:1 ]",
]
`;
exports[`runs examples > example "fmdecay" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
]
`;
exports[`runs examples > example "fmenv" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
]
`;
exports[`runs examples > example "fmh" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmh:1 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmh:1 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmh:1 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmh:1 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmh:2 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmh:2 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmh:2 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmh:2 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmh:1.5 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmh:1.5 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmh:1.5 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmh:1.5 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmh:1.61 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmh:1.61 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmh:1.61 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmh:1.61 ]",
"[ 0/1 → 1/4 | note:c fmi:4 fmh:1 analyze:1 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmh:1 analyze:1 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmh:1 analyze:1 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmh:1 analyze:1 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmh:2 analyze:1 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmh:2 analyze:1 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmh:2 analyze:1 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmh:2 analyze:1 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmh:1.5 analyze:1 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmh:1.5 analyze:1 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmh:1.5 analyze:1 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmh:1.5 analyze:1 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmh:1.61 analyze:1 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmh:1.61 analyze:1 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmh:1.61 analyze:1 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmh:1.61 analyze:1 ]",
]
`;
exports[`runs examples > example "fmsustain" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
]
`;
@ -1929,6 +2067,15 @@ exports[`runs examples > example "freq" example index 1 1`] = `
]
`;
exports[`runs examples > example "ftype" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth cutoff:500 bpenv:4 ftype:12db ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth cutoff:500 bpenv:4 ftype:24db ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth cutoff:500 bpenv:4 ftype:12db ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth cutoff:500 bpenv:4 ftype:24db ]",
]
`;
exports[`runs examples > example "gain" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh gain:0.4 ]",
@ -1966,6 +2113,33 @@ exports[`runs examples > example "gain" example index 0 1`] = `
]
`;
exports[`runs examples > example "hpattack" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
]
`;
exports[`runs examples > example "hpdecay" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0.2 hpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0.2 hpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0.2 hpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0.2 hpenv:4 ]",
]
`;
exports[`runs examples > example "hpenv" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
]
`;
exports[`runs examples > example "hpf" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:hh hcutoff:4000 ]",
@ -2053,6 +2227,24 @@ exports[`runs examples > example "hpq" example index 0 1`] = `
]
`;
exports[`runs examples > example "hprelease" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth clip:0.5 hcutoff:500 hpenv:4 hprelease:0.5 release:0.5 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth clip:0.5 hcutoff:500 hpenv:4 hprelease:0.5 release:0.5 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth clip:0.5 hcutoff:500 hpenv:4 hprelease:0.5 release:0.5 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth clip:0.5 hcutoff:500 hpenv:4 hprelease:0.5 release:0.5 ]",
]
`;
exports[`runs examples > example "hpsustain" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0 hpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0 hpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0 hpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0 hpenv:4 ]",
]
`;
exports[`runs examples > example "hurry" example index 0 1`] = `
[
"[ 0/1 → 3/4 | s:bd speed:1 ]",
@ -2457,10 +2649,10 @@ exports[`runs examples > example "linger" example index 0 1`] = `
exports[`runs examples > example "loop" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:bd loop:1 ]",
"[ 1/1 → 2/1 | s:bd loop:2 ]",
"[ 2/1 → 3/1 | s:bd loop:3 ]",
"[ 3/1 → 4/1 | s:bd loop:4 ]",
"[ 0/1 → 1/1 | s:casio loop:1 ]",
"[ 1/1 → 2/1 | s:casio loop:1 ]",
"[ 2/1 → 3/1 | s:casio loop:1 ]",
"[ 3/1 → 4/1 | s:casio loop:1 ]",
]
`;
@ -2482,6 +2674,51 @@ exports[`runs examples > example "loopAtCps" example index 0 1`] = `
]
`;
exports[`runs examples > example "loopBegin" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:space loop:1 loopBegin:0 analyze:1 ]",
"[ 1/1 → 2/1 | s:space loop:1 loopBegin:0.125 analyze:1 ]",
"[ 2/1 → 3/1 | s:space loop:1 loopBegin:0.25 analyze:1 ]",
"[ 3/1 → 4/1 | s:space loop:1 loopBegin:0 analyze:1 ]",
]
`;
exports[`runs examples > example "loopEnd" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:space loop:1 loopEnd:1 analyze:1 ]",
"[ 1/1 → 2/1 | s:space loop:1 loopEnd:0.75 analyze:1 ]",
"[ 2/1 → 3/1 | s:space loop:1 loopEnd:0.5 analyze:1 ]",
"[ 3/1 → 4/1 | s:space loop:1 loopEnd:0.25 analyze:1 ]",
]
`;
exports[`runs examples > example "lpattack" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
]
`;
exports[`runs examples > example "lpdecay" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0.2 lpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0.2 lpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0.2 lpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0.2 lpenv:4 ]",
]
`;
exports[`runs examples > example "lpenv" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
]
`;
exports[`runs examples > example "lpf" example index 0 1`] = `
[
"[ 0/1 → 1/3 | s:hh cutoff:4000 ]",
@ -2573,6 +2810,24 @@ exports[`runs examples > example "lpq" example index 0 1`] = `
]
`;
exports[`runs examples > example "lprelease" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth clip:0.5 cutoff:500 lpenv:4 lprelease:0.5 release:0.5 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth clip:0.5 cutoff:500 lpenv:4 lprelease:0.5 release:0.5 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth clip:0.5 cutoff:500 lpenv:4 lprelease:0.5 release:0.5 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth clip:0.5 cutoff:500 lpenv:4 lprelease:0.5 release:0.5 ]",
]
`;
exports[`runs examples > example "lpsustain" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0 lpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0 lpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0 lpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0 lpenv:4 ]",
]
`;
exports[`runs examples > example "lrate" example index 0 1`] = `
[
"[ 0/1 → 1/1 | n:0 s:supersquare leslie:1 lrate:1 ]",
@ -4095,6 +4350,36 @@ exports[`runs examples > example "speed" example index 1 1`] = `
]
`;
exports[`runs examples > example "splice" example index 0 1`] = `
[
"[ 0/1 → 5/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 5/26 → 5/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 5/13 → 20/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 20/39 → 25/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 25/39 → 10/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 10/13 → 25/26 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ (25/26 → 1/1) ⇝ 35/26 | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 25/26 ⇜ (1/1 → 35/26) | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 35/26 → 20/13 | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 20/13 → 45/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 45/26 → 25/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ (25/13 → 2/1) ⇝ 80/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 25/13 ⇜ (2/1 → 80/39) | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 80/39 → 85/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 85/39 → 30/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 30/13 → 5/2 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 5/2 → 75/26 | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ (75/26 → 3/1) ⇝ 40/13 | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 75/26 ⇜ (3/1 → 40/13) | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 40/13 → 85/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 85/26 → 45/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 45/13 → 140/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 140/39 → 145/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 145/39 → 50/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ (50/13 → 4/1) ⇝ 105/26 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
]
`;
exports[`runs examples > example "square" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:C3 ]",
@ -4473,6 +4758,42 @@ exports[`runs examples > example "velocity" example index 0 1`] = `
]
`;
exports[`runs examples > example "vib" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:a vib:0.5 ]",
"[ 1/1 → 2/1 | note:a vib:1 ]",
"[ 2/1 → 3/1 | note:a vib:2 ]",
"[ 3/1 → 4/1 | note:a vib:4 ]",
]
`;
exports[`runs examples > example "vib" example index 1 1`] = `
[
"[ 0/1 → 1/1 | note:a vib:0.5 vibmod:12 ]",
"[ 1/1 → 2/1 | note:a vib:1 vibmod:12 ]",
"[ 2/1 → 3/1 | note:a vib:2 vibmod:12 ]",
"[ 3/1 → 4/1 | note:a vib:4 vibmod:12 ]",
]
`;
exports[`runs examples > example "vibmod" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:a vib:4 vibmod:0.25 ]",
"[ 1/1 → 2/1 | note:a vib:4 vibmod:0.5 ]",
"[ 2/1 → 3/1 | note:a vib:4 vibmod:1 ]",
"[ 3/1 → 4/1 | note:a vib:4 vibmod:2 ]",
]
`;
exports[`runs examples > example "vibmod" example index 1 1`] = `
[
"[ 0/1 → 1/1 | note:a vibmod:0.25 vib:8 ]",
"[ 1/1 → 2/1 | note:a vibmod:0.5 vib:8 ]",
"[ 2/1 → 3/1 | note:a vibmod:1 vib:8 ]",
"[ 3/1 → 4/1 | note:a vibmod:2 vib:8 ]",
]
`;
exports[`runs examples > example "voicing" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:E4 ]",

View File

@ -3,23 +3,23 @@
exports[`renders tunes > tune: amensister 1`] = `
[
"[ 0/1 → 1/16 | s:breath room:1 shape:0.6 begin:0.9375 end:1 ]",
"[ 0/1 → 1/8 | note:Eb1 s:sawtooth decay:0.1 sustain:0 gain:0.4 cutoff:300.0066107586751 resonance:10 ]",
"[ 0/1 → 1/8 | note:F1 s:sawtooth decay:0.1 sustain:0 gain:0.4 cutoff:300.0066107586751 resonance:10 ]",
"[ 0/1 → 1/8 | note:Eb1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.0066107586751 ]",
"[ 0/1 → 1/8 | note:F1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.0066107586751 ]",
"[ 0/1 → 1/4 | n:0 s:amencutup room:0.5 ]",
"[ 1/16 → 1/8 | s:breath room:1 shape:0.6 begin:0.875 end:0.9375 ]",
"[ 1/8 → 3/16 | s:breath room:1 shape:0.6 begin:0.8125 end:0.875 ]",
"[ 1/8 → 1/4 | note:45 s:sawtooth decay:0.1 sustain:0 gain:0.4 cutoff:300.174310575404 resonance:10 ]",
"[ 1/8 → 1/4 | note:45 s:sawtooth decay:0.1 sustain:0 gain:0.4 cutoff:300.174310575404 resonance:10 ]",
"[ 1/8 → 1/4 | note:45 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.174310575404 ]",
"[ 1/8 → 1/4 | note:45 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.174310575404 ]",
"[ 3/16 → 1/4 | s:breath room:1 shape:0.6 begin:0.75 end:0.8125 ]",
"[ 1/4 → 5/16 | s:breath room:1 shape:0.6 begin:0.6875 end:0.75 ]",
"[ 1/4 → 3/8 | n:1 speed:2 delay:0.5 s:amencutup room:0.5 ]",
"[ 1/4 → 3/8 | note:A1 s:sawtooth decay:0.1 sustain:0 gain:0.4 cutoff:300.7878869297153 resonance:10 ]",
"[ 1/4 → 3/8 | note:A1 s:sawtooth decay:0.1 sustain:0 gain:0.4 cutoff:300.7878869297153 resonance:10 ]",
"[ 1/4 → 3/8 | note:A1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.7878869297153 ]",
"[ 1/4 → 3/8 | note:A1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.7878869297153 ]",
"[ 5/16 → 3/8 | s:breath room:1 shape:0.6 begin:0.625 end:0.6875 ]",
"[ 3/8 → 7/16 | s:breath room:1 shape:0.6 begin:0.5625 end:0.625 ]",
"[ 3/8 → 1/2 | n:1 s:amencutup room:0.5 ]",
"[ 3/8 → 1/2 | note:F1 s:sawtooth decay:0.1 sustain:0 gain:0.4 cutoff:302.11020572391345 resonance:10 ]",
"[ 3/8 → 1/2 | note:F1 s:sawtooth decay:0.1 sustain:0 gain:0.4 cutoff:302.11020572391345 resonance:10 ]",
"[ 3/8 → 1/2 | note:F1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:302.11020572391345 ]",
"[ 3/8 → 1/2 | note:F1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:302.11020572391345 ]",
"[ 7/16 → 1/2 | s:breath room:1 shape:0.6 begin:0.5 end:0.5625 ]",
"[ 1/2 → 9/16 | s:breath room:1 shape:0.6 begin:0.4375 end:0.5 ]",
"[ 1/2 → 3/4 | n:2 s:amencutup room:0.5 ]",
@ -28,13 +28,13 @@ exports[`renders tunes > tune: amensister 1`] = `
"[ 11/16 → 3/4 | s:breath room:1 shape:0.6 begin:0.25 end:0.3125 ]",
"[ 3/4 → 13/16 | s:breath room:1 shape:0.6 begin:0.1875 end:0.25 ]",
"[ 3/4 → 7/8 | n:3 s:amencutup room:0.5 ]",
"[ 3/4 → 7/8 | note:Bb0 s:sawtooth decay:0.1 sustain:0 gain:0.4 cutoff:312.54769231985796 resonance:10 ]",
"[ 3/4 → 7/8 | note:Bb0 s:sawtooth decay:0.1 sustain:0 gain:0.4 cutoff:312.54769231985796 resonance:10 ]",
"[ 3/4 → 7/8 | note:Bb0 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:312.54769231985796 ]",
"[ 3/4 → 7/8 | note:Bb0 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:312.54769231985796 ]",
"[ 13/16 → 7/8 | s:breath room:1 shape:0.6 begin:0.125 end:0.1875 ]",
"[ 7/8 → 15/16 | s:breath room:1 shape:0.6 begin:0.0625 end:0.125 ]",
"[ 7/8 → 1/1 | n:3 s:amencutup room:0.5 ]",
"[ 7/8 → 1/1 | note:D1 s:sawtooth decay:0.1 sustain:0 gain:0.4 cutoff:318.7927796831686 resonance:10 ]",
"[ 7/8 → 1/1 | note:D1 s:sawtooth decay:0.1 sustain:0 gain:0.4 cutoff:318.7927796831686 resonance:10 ]",
"[ 7/8 → 1/1 | note:D1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:318.7927796831686 ]",
"[ 7/8 → 1/1 | note:D1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:318.7927796831686 ]",
"[ 15/16 → 1/1 | s:breath room:1 shape:0.6 begin:0 end:0.0625 ]",
]
`;
@ -44,8 +44,8 @@ exports[`renders tunes > tune: arpoon 1`] = `
"[ (0/1 → 1/4) ⇝ 1/3 | note:55.000070545713186 clip:2 cutoff:501.2345499807435 resonance:12 gain:0.5 decay:0.16 sustain:0.5 delay:0.2 room:0.5 pan:0.48882285676537807 s:piano ]",
"[ (0/1 → 1/4) ⇝ 1/3 | note:64.00007054571319 clip:2 cutoff:501.2345499807435 resonance:12 gain:0.5 decay:0.16 sustain:0.5 delay:0.2 room:0.5 pan:0.48882285676537807 s:piano ]",
"[ 0/1 → 1/2 | s:bd bank:RolandTR909 gain:0.5 ]",
"[ 0/1 → 1/1 | note:33 s:sawtooth clip:1 cutoff:300 ]",
"[ 0/1 → 1/1 | note:33.12 s:sawtooth clip:1 cutoff:300 ]",
"[ 0/1 → 1/1 | note:33 s:sawtooth cutoff:180 lpattack:0.1 lpenv:2 ]",
"[ 0/1 → 1/1 | note:33.12 s:sawtooth cutoff:180 lpattack:0.1 lpenv:2 ]",
"[ 0/1 ⇜ (1/4 → 1/3) | note:55.000070545713186 clip:2 cutoff:501.2345499807435 resonance:12 gain:0.8 decay:0.16 sustain:0.5 delay:0.2 room:0.5 pan:0.48882285676537807 s:piano ]",
"[ 0/1 ⇜ (1/4 → 1/3) | note:64.00007054571319 clip:2 cutoff:501.2345499807435 resonance:12 gain:0.8 decay:0.16 sustain:0.5 delay:0.2 room:0.5 pan:0.48882285676537807 s:piano ]",
"[ (1/3 → 1/2) ⇝ 2/3 | note:60.00166796373806 clip:2 cutoff:529.1893654159594 resonance:12 gain:0.8 decay:0.16 sustain:0.5 delay:0.2 room:0.5 pan:0.5560660171779821 s:piano ]",
@ -323,8 +323,8 @@ exports[`renders tunes > tune: blippyRhodes 1`] = `
[
"[ 0/1 → 1/6 | note:G4 clip:0.3 s:rhodes room:0.5 delay:0.3 delayfeedback:0.4 delaytime:0.08333333333333333 gain:0.5 ]",
"[ 0/1 → 1/3 | s:bd ]",
"[ (0/1 → 2/3) ⇝ 4/3 | note:36 gain:0.3 clip:1 s:sawtooth cutoff:600 ]",
"[ (0/1 → 2/3) ⇝ 4/3 | note:36.02 gain:0.3 clip:1 s:sawtooth cutoff:600 ]",
"[ (0/1 → 2/3) ⇝ 4/3 | note:36 gain:0.3 clip:1 cutoff:600 lpattack:0.2 lpenv:-4 s:sawtooth ]",
"[ (0/1 → 2/3) ⇝ 4/3 | note:36.02 gain:0.3 clip:1 cutoff:600 lpattack:0.2 lpenv:-4 s:sawtooth ]",
"[ 1/6 → 1/3 | note:G4 clip:0.3 s:rhodes room:0.5 delay:0.3 delayfeedback:0.4 delaytime:0.08333333333333333 gain:0.5 ]",
"[ 1/3 → 1/2 | note:B3 clip:0.3 s:rhodes room:0.5 delay:0.3 delayfeedback:0.4 delaytime:0.08333333333333333 gain:0.5 ]",
"[ 1/3 → 1/2 | note:E4 clip:0.3 s:rhodes room:0.5 delay:0.3 delayfeedback:0.4 delaytime:0.08333333333333333 gain:0.5 ]",
@ -332,8 +332,8 @@ exports[`renders tunes > tune: blippyRhodes 1`] = `
"[ 1/2 → 2/3 | note:B3 clip:0.3 s:rhodes room:0.5 delay:0.3 delayfeedback:0.4 delaytime:0.08333333333333333 gain:0.5 ]",
"[ 1/2 → 2/3 | note:E4 clip:0.3 s:rhodes room:0.5 delay:0.3 delayfeedback:0.4 delaytime:0.08333333333333333 gain:0.5 ]",
"[ 2/3 → 5/6 | note:G3 clip:0.3 s:rhodes room:0.5 delay:0.3 delayfeedback:0.4 delaytime:0.08333333333333333 gain:0.5 ]",
"[ 0/1 ⇜ (2/3 → 1/1) ⇝ 4/3 | note:36 gain:0.3 clip:1 s:sawtooth cutoff:600 ]",
"[ 0/1 ⇜ (2/3 → 1/1) ⇝ 4/3 | note:36.02 gain:0.3 clip:1 s:sawtooth cutoff:600 ]",
"[ 0/1 ⇜ (2/3 → 1/1) ⇝ 4/3 | note:36 gain:0.3 clip:1 cutoff:600 lpattack:0.2 lpenv:-4 s:sawtooth ]",
"[ 0/1 ⇜ (2/3 → 1/1) ⇝ 4/3 | note:36.02 gain:0.3 clip:1 cutoff:600 lpattack:0.2 lpenv:-4 s:sawtooth ]",
"[ 2/3 → 1/1 | s:sn ]",
"[ 5/6 → 1/1 | note:G3 clip:0.3 s:rhodes room:0.5 delay:0.3 delayfeedback:0.4 delaytime:0.08333333333333333 gain:0.5 ]",
]
@ -7315,20 +7315,20 @@ exports[`renders tunes > tune: flatrave 1`] = `
"[ 0/1 ⇜ (1/8 → 1/4) | s:hh n:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | s:hh n:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | s:hh n:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
"[ 1/8 → 1/4 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
"[ 1/4 → 3/8 | s:hh n:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/4 → 3/8 | s:hh n:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/4 → 3/8 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
"[ 1/4 → 3/8 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
"[ 3/8 → 1/2 | s:hh n:1 end:0.020023446730265706 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/2 → 5/8 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
"[ 1/2 → 5/8 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
"[ 1/2 → 1/1 | s:bd bank:RolandTR909 ]",
"[ 1/2 → 1/1 | s:cp bank:RolandTR909 ]",
"[ 1/2 → 1/1 | s:sd bank:RolandTR909 ]",
"[ 5/8 → 3/4 | s:hh n:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 5/8 → 3/4 | s:hh n:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 5/8 → 3/4 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
"[ 5/8 → 3/4 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
"[ 3/4 → 7/8 | s:hh n:1 end:0.02013941880355398 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 7/8 → 1/1 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
"[ 7/8 → 1/1 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
]
`;
@ -7722,8 +7722,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ -1/8 ⇜ (0/1 → 3/8) | note:B3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:1699.6897509708342 ]",
"[ -1/4 ⇜ (1/12 → 1/8) | note:A5 s:sawtooth gain:0.2536811842784369 attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]",
"[ -1/4 ⇜ (1/12 → 1/8) | note:C#5 s:sawtooth gain:0.2536811842784369 attack:0.001 decay:0.2 sustain:0 hcutoff:5999.785818935017 cutoff:4000 ]",
"[ 1/8 → 1/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1699.6897509708342 ]",
"[ 1/8 → 1/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1699.6897509708342 ]",
"[ 1/8 → 1/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1699.6897509708342 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 1/8 → 1/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1699.6897509708342 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (1/8 → 5/12) ⇝ 1/2 | note:A5 s:sawtooth gain:0.26836160127988246 attack:0.001 decay:0.2 sustain:0 hcutoff:5994.647308096509 cutoff:4000 ]",
"[ (1/8 → 5/12) ⇝ 1/2 | note:C#5 s:sawtooth gain:0.26836160127988246 attack:0.001 decay:0.2 sustain:0 hcutoff:5994.647308096509 cutoff:4000 ]",
"[ -1/8 ⇜ (1/6 → 1/4) | note:F#5 s:sawtooth gain:0.2573601511491127 attack:0.001 decay:0.2 sustain:0 hcutoff:5999.143312438893 cutoff:4000 ]",
@ -7735,14 +7735,14 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (1/4 → 7/12) ⇝ 5/8 | note:E5 s:sawtooth gain:0.2756442833140452 attack:0.001 decay:0.2 sustain:0 hcutoff:5989.512318936654 cutoff:4000 ]",
"[ 0/1 ⇜ (1/3 → 3/8) | note:A5 s:sawtooth gain:0.26103468453995016 attack:0.001 decay:0.2 sustain:0 hcutoff:5998.072590601808 cutoff:4000 ]",
"[ 0/1 ⇜ (1/3 → 3/8) | note:C#5 s:sawtooth gain:0.26103468453995016 attack:0.001 decay:0.2 sustain:0 hcutoff:5998.072590601808 cutoff:4000 ]",
"[ 3/8 → 1/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1765.826371664994 ]",
"[ 3/8 → 1/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1765.826371664994 ]",
"[ 3/8 → 1/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1765.826371664994 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 3/8 → 1/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1765.826371664994 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (3/8 → 2/3) ⇝ 3/4 | note:A5 s:sawtooth gain:0.2828651860235305 attack:0.001 decay:0.2 sustain:0 hcutoff:5982.671142387316 cutoff:4000 ]",
"[ (3/8 → 2/3) ⇝ 3/4 | note:C#5 s:sawtooth gain:0.2828651860235305 attack:0.001 decay:0.2 sustain:0 hcutoff:5982.671142387316 cutoff:4000 ]",
"[ 1/8 ⇜ (5/12 → 1/2) | note:F#5 s:sawtooth gain:0.26836160127988246 attack:0.001 decay:0.2 sustain:0 hcutoff:5994.647308096509 cutoff:4000 ]",
"[ 1/8 ⇜ (5/12 → 1/2) | note:A4 s:sawtooth gain:0.26836160127988246 attack:0.001 decay:0.2 sustain:0 hcutoff:5994.647308096509 cutoff:4000 ]",
"[ 1/2 → 5/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1798.799979846742 ]",
"[ 1/2 → 5/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1798.799979846742 ]",
"[ 1/2 → 5/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1798.799979846742 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 1/2 → 5/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1798.799979846742 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 1/2 → 3/4 | note:F#5 s:sawtooth gain:0.28644702698548963 attack:0.001 decay:0.2 sustain:0 hcutoff:5978.612153434527 cutoff:4000 ]",
"[ 1/2 → 3/4 | note:A4 s:sawtooth gain:0.28644702698548963 attack:0.001 decay:0.2 sustain:0 hcutoff:5978.612153434527 cutoff:4000 ]",
"[ 1/2 → 3/4 | s:bd gain:0.7 ]",
@ -7755,8 +7755,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (5/8 → 11/12) ⇝ 1/1 | note:C#5 s:sawtooth gain:0.29705226105983373 attack:0.001 decay:0.2 sustain:0 hcutoff:5963.890147645195 cutoff:4000 ]",
"[ 3/8 ⇜ (2/3 → 3/4) | note:F#5 s:sawtooth gain:0.2828651860235305 attack:0.001 decay:0.2 sustain:0 hcutoff:5982.671142387316 cutoff:4000 ]",
"[ 3/8 ⇜ (2/3 → 3/4) | note:A4 s:sawtooth gain:0.2828651860235305 attack:0.001 decay:0.2 sustain:0 hcutoff:5982.671142387316 cutoff:4000 ]",
"[ 3/4 → 7/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1864.4584935007128 ]",
"[ 3/4 → 7/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1864.4584935007128 ]",
"[ 3/4 → 7/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1864.4584935007128 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 3/4 → 7/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1864.4584935007128 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 3/4 → 1/1 | note:F#5 s:sawtooth gain:0.300533478008833 attack:0.001 decay:0.2 sustain:0 hcutoff:5958.137268909887 cutoff:4000 ]",
"[ 3/4 → 1/1 | note:A4 s:sawtooth gain:0.300533478008833 attack:0.001 decay:0.2 sustain:0 hcutoff:5958.137268909887 cutoff:4000 ]",
"[ 3/4 → 1/1 | s:hh3 gain:0.7 ]",
@ -7764,8 +7764,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (3/4 → 1/1) ⇝ 9/8 | note:E5 s:sawtooth gain:0.30398425548024827 attack:0.001 decay:0.2 sustain:0 hcutoff:5951.963201008076 cutoff:4000 ]",
"[ 1/2 ⇜ (5/6 → 7/8) | note:A5 s:sawtooth gain:0.29000691362123476 attack:0.001 decay:0.2 sustain:0 hcutoff:5974.128467049176 cutoff:4000 ]",
"[ 1/2 ⇜ (5/6 → 7/8) | note:C#5 s:sawtooth gain:0.29000691362123476 attack:0.001 decay:0.2 sustain:0 hcutoff:5974.128467049176 cutoff:4000 ]",
"[ 7/8 → 1/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1897.1038487394403 ]",
"[ 7/8 → 1/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1897.1038487394403 ]",
"[ 7/8 → 1/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1897.1038487394403 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 7/8 → 1/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1897.1038487394403 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (7/8 → 1/1) ⇝ 5/4 | note:A5 s:sawtooth gain:0.3107861971007485 attack:0.001 decay:0.2 sustain:0 hcutoff:5938.355801271282 cutoff:4000 ]",
"[ (7/8 → 1/1) ⇝ 5/4 | note:C#5 s:sawtooth gain:0.3107861971007485 attack:0.001 decay:0.2 sustain:0 hcutoff:5938.355801271282 cutoff:4000 ]",
"[ 5/8 ⇜ (11/12 → 1/1) | note:F#5 s:sawtooth gain:0.29705226105983373 attack:0.001 decay:0.2 sustain:0 hcutoff:5963.890147645195 cutoff:4000 ]",
@ -7781,8 +7781,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (1/1 → 4/3) ⇝ 11/8 | note:E5 s:sawtooth gain:0.317441699448191 attack:0.001 decay:0.2 sustain:0 hcutoff:5923.077274266886 cutoff:4000 ]",
"[ 3/4 ⇜ (13/12 → 9/8) | note:A5 s:sawtooth gain:0.30398425548024827 attack:0.001 decay:0.2 sustain:0 hcutoff:5951.963201008076 cutoff:4000 ]",
"[ 3/4 ⇜ (13/12 → 9/8) | note:C#5 s:sawtooth gain:0.30398425548024827 attack:0.001 decay:0.2 sustain:0 hcutoff:5951.963201008076 cutoff:4000 ]",
"[ 9/8 → 5/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1961.928446178906 ]",
"[ 9/8 → 5/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1961.928446178906 ]",
"[ 9/8 → 5/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1961.928446178906 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 9/8 → 5/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:1961.928446178906 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (9/8 → 17/12) ⇝ 3/2 | note:A5 s:sawtooth gain:0.3239347288344676 attack:0.001 decay:0.2 sustain:0 hcutoff:5906.1380911341175 cutoff:4000 ]",
"[ (9/8 → 17/12) ⇝ 3/2 | note:C#5 s:sawtooth gain:0.3239347288344676 attack:0.001 decay:0.2 sustain:0 hcutoff:5906.1380911341175 cutoff:4000 ]",
"[ 7/8 ⇜ (7/6 → 5/4) | note:F#5 s:sawtooth gain:0.3107861971007485 attack:0.001 decay:0.2 sustain:0 hcutoff:5938.355801271282 cutoff:4000 ]",
@ -7794,14 +7794,14 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (5/4 → 19/12) ⇝ 13/8 | note:E5 s:sawtooth gain:0.3302496429830646 attack:0.001 decay:0.2 sustain:0 hcutoff:5887.549861142967 cutoff:4000 ]",
"[ 1/1 ⇜ (4/3 → 11/8) | note:A5 s:sawtooth gain:0.317441699448191 attack:0.001 decay:0.2 sustain:0 hcutoff:5923.077274266886 cutoff:4000 ]",
"[ 1/1 ⇜ (4/3 → 11/8) | note:C#5 s:sawtooth gain:0.317441699448191 attack:0.001 decay:0.2 sustain:0 hcutoff:5923.077274266886 cutoff:4000 ]",
"[ 11/8 → 3/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2026.0015806698216 ]",
"[ 11/8 → 3/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2026.0015806698216 ]",
"[ 11/8 → 3/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2026.0015806698216 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 11/8 → 3/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2026.0015806698216 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (11/8 → 5/3) ⇝ 7/4 | note:A5 s:sawtooth gain:0.3363712287126769 attack:0.001 decay:0.2 sustain:0 hcutoff:5867.325323737765 cutoff:4000 ]",
"[ (11/8 → 5/3) ⇝ 7/4 | note:C#5 s:sawtooth gain:0.3363712287126769 attack:0.001 decay:0.2 sustain:0 hcutoff:5867.325323737765 cutoff:4000 ]",
"[ 9/8 ⇜ (17/12 → 3/2) | note:F#5 s:sawtooth gain:0.3239347288344676 attack:0.001 decay:0.2 sustain:0 hcutoff:5906.1380911341175 cutoff:4000 ]",
"[ 9/8 ⇜ (17/12 → 3/2) | note:A4 s:sawtooth gain:0.3239347288344676 attack:0.001 decay:0.2 sustain:0 hcutoff:5906.1380911341175 cutoff:4000 ]",
"[ 3/2 → 13/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2057.708031580958 ]",
"[ 3/2 → 13/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2057.708031580958 ]",
"[ 3/2 → 13/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2057.708031580958 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 3/2 → 13/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2057.708031580958 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 3/2 → 7/4 | note:F#5 s:sawtooth gain:0.339354895673865 attack:0.001 decay:0.2 sustain:0 hcutoff:5856.603727730447 cutoff:4000 ]",
"[ 3/2 → 7/4 | note:A4 s:sawtooth gain:0.339354895673865 attack:0.001 decay:0.2 sustain:0 hcutoff:5856.603727730447 cutoff:4000 ]",
"[ 3/2 → 7/4 | s:bd gain:0.7 ]",
@ -7818,8 +7818,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (13/8 → 2/1) ⇝ 17/8 | note:A3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2120.3652183367367 ]",
"[ 11/8 ⇜ (5/3 → 7/4) | note:F#5 s:sawtooth gain:0.3363712287126769 attack:0.001 decay:0.2 sustain:0 hcutoff:5867.325323737765 cutoff:4000 ]",
"[ 11/8 ⇜ (5/3 → 7/4) | note:A4 s:sawtooth gain:0.3363712287126769 attack:0.001 decay:0.2 sustain:0 hcutoff:5867.325323737765 cutoff:4000 ]",
"[ 7/4 → 15/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2120.3652183367367 ]",
"[ 7/4 → 15/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2120.3652183367367 ]",
"[ 7/4 → 15/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2120.3652183367367 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 7/4 → 15/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2120.3652183367367 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 7/4 → 2/1 | note:F#5 s:sawtooth gain:0.3507338432270528 attack:0.001 decay:0.2 sustain:0 hcutoff:5809.698831278217 cutoff:4000 ]",
"[ 7/4 → 2/1 | note:A4 s:sawtooth gain:0.3507338432270528 attack:0.001 decay:0.2 sustain:0 hcutoff:5809.698831278217 cutoff:4000 ]",
"[ 7/4 → 2/1 | s:hh3 gain:0.7 ]",
@ -7829,8 +7829,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (7/4 → 2/1) ⇝ 9/4 | note:A3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2135.8582993222344 ]",
"[ 3/2 ⇜ (11/6 → 15/8) | note:A5 s:sawtooth gain:0.3422847385870941 attack:0.001 decay:0.2 sustain:0 hcutoff:5845.47833980621 cutoff:4000 ]",
"[ 3/2 ⇜ (11/6 → 15/8) | note:C#5 s:sawtooth gain:0.3422847385870941 attack:0.001 decay:0.2 sustain:0 hcutoff:5845.47833980621 cutoff:4000 ]",
"[ 15/8 → 2/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2151.2782118349805 ]",
"[ 15/8 → 2/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2151.2782118349805 ]",
"[ 15/8 → 2/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2151.2782118349805 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 15/8 → 2/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2151.2782118349805 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (15/8 → 2/1) ⇝ 9/4 | note:A5 s:sawtooth gain:0.35343108171056004 attack:0.001 decay:0.2 sustain:0 hcutoff:5796.978025372246 cutoff:4000 ]",
"[ (15/8 → 2/1) ⇝ 9/4 | note:C#5 s:sawtooth gain:0.35343108171056004 attack:0.001 decay:0.2 sustain:0 hcutoff:5796.978025372246 cutoff:4000 ]",
"[ (15/8 → 2/1) ⇝ 19/8 | note:F#3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2151.2782118349805 ]",
@ -7854,8 +7854,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ 15/8 ⇜ (2/1 → 19/8) | note:A3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2212.17990613181 ]",
"[ 7/4 ⇜ (25/12 → 17/8) | note:A5 s:sawtooth gain:0.3586370624427201 attack:0.001 decay:0.2 sustain:0 hcutoff:5770.357934562703 cutoff:4000 ]",
"[ 7/4 ⇜ (25/12 → 17/8) | note:C#5 s:sawtooth gain:0.3586370624427201 attack:0.001 decay:0.2 sustain:0 hcutoff:5770.357934562703 cutoff:4000 ]",
"[ 17/8 → 9/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2212.17990613181 ]",
"[ 17/8 → 9/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2212.17990613181 ]",
"[ 17/8 → 9/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2212.17990613181 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 17/8 → 9/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2212.17990613181 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (17/8 → 29/12) ⇝ 5/2 | note:A5 s:sawtooth gain:0.368251964143991 attack:0.001 decay:0.2 sustain:0 hcutoff:5712.469093657604 cutoff:4000 ]",
"[ (17/8 → 29/12) ⇝ 5/2 | note:C#5 s:sawtooth gain:0.368251964143991 attack:0.001 decay:0.2 sustain:0 hcutoff:5712.469093657604 cutoff:4000 ]",
"[ 15/8 ⇜ (13/6 → 9/4) | note:F#5 s:sawtooth gain:0.36114266880324397 attack:0.001 decay:0.2 sustain:0 hcutoff:5756.463210874651 cutoff:4000 ]",
@ -7867,14 +7867,14 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (9/4 → 31/12) ⇝ 21/8 | note:E5 s:sawtooth gain:0.3726377219727376 attack:0.001 decay:0.2 sustain:0 hcutoff:5681.240017681994 cutoff:4000 ]",
"[ 2/1 ⇜ (7/3 → 19/8) | note:A5 s:sawtooth gain:0.3635813269759728 attack:0.001 decay:0.2 sustain:0 hcutoff:5742.18185383172 cutoff:4000 ]",
"[ 2/1 ⇜ (7/3 → 19/8) | note:C#5 s:sawtooth gain:0.3635813269759728 attack:0.001 decay:0.2 sustain:0 hcutoff:5742.18185383172 cutoff:4000 ]",
"[ 19/8 → 5/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2271.727259793624 ]",
"[ 19/8 → 5/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2271.727259793624 ]",
"[ 19/8 → 5/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2271.727259793624 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 19/8 → 5/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2271.727259793624 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (19/8 → 8/3) ⇝ 11/4 | note:A5 s:sawtooth gain:0.3767280347874561 attack:0.001 decay:0.2 sustain:0 hcutoff:5648.516028753632 cutoff:4000 ]",
"[ (19/8 → 8/3) ⇝ 11/4 | note:C#5 s:sawtooth gain:0.3767280347874561 attack:0.001 decay:0.2 sustain:0 hcutoff:5648.516028753632 cutoff:4000 ]",
"[ 17/8 ⇜ (29/12 → 5/2) | note:F#5 s:sawtooth gain:0.368251964143991 attack:0.001 decay:0.2 sustain:0 hcutoff:5712.469093657604 cutoff:4000 ]",
"[ 17/8 ⇜ (29/12 → 5/2) | note:A4 s:sawtooth gain:0.368251964143991 attack:0.001 decay:0.2 sustain:0 hcutoff:5712.469093657604 cutoff:4000 ]",
"[ 5/2 → 21/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2300.948092306816 ]",
"[ 5/2 → 21/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2300.948092306816 ]",
"[ 5/2 → 21/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2300.948092306816 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 5/2 → 21/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2300.948092306816 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 5/2 → 11/4 | note:F#5 s:sawtooth gain:0.37865929150004085 attack:0.001 decay:0.2 sustain:0 hcutoff:5631.60041088523 cutoff:4000 ]",
"[ 5/2 → 11/4 | note:A4 s:sawtooth gain:0.37865929150004085 attack:0.001 decay:0.2 sustain:0 hcutoff:5631.60041088523 cutoff:4000 ]",
"[ 5/2 → 11/4 | s:bd gain:0.7 ]",
@ -7887,8 +7887,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (21/8 → 35/12) ⇝ 3/1 | note:C#5 s:sawtooth gain:0.38398364517932737 attack:0.001 decay:0.2 sustain:0 hcutoff:5578.674030756363 cutoff:4000 ]",
"[ 19/8 ⇜ (8/3 → 11/4) | note:F#5 s:sawtooth gain:0.3767280347874561 attack:0.001 decay:0.2 sustain:0 hcutoff:5648.516028753632 cutoff:4000 ]",
"[ 19/8 ⇜ (8/3 → 11/4) | note:A4 s:sawtooth gain:0.3767280347874561 attack:0.001 decay:0.2 sustain:0 hcutoff:5648.516028753632 cutoff:4000 ]",
"[ 11/4 → 23/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2358.1960716159333 ]",
"[ 11/4 → 23/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2358.1960716159333 ]",
"[ 11/4 → 23/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2358.1960716159333 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 11/4 → 23/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2358.1960716159333 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 11/4 → 3/1 | note:F#5 s:sawtooth gain:0.3855983939685166 attack:0.001 decay:0.2 sustain:0 hcutoff:5560.31547155504 cutoff:4000 ]",
"[ 11/4 → 3/1 | note:A4 s:sawtooth gain:0.3855983939685166 attack:0.001 decay:0.2 sustain:0 hcutoff:5560.31547155504 cutoff:4000 ]",
"[ 11/4 → 3/1 | s:hh3 gain:0.7 ]",
@ -7896,8 +7896,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (11/4 → 3/1) ⇝ 25/8 | note:E5 s:sawtooth gain:0.3871314633555296 attack:0.001 decay:0.2 sustain:0 hcutoff:5541.603887904197 cutoff:4000 ]",
"[ 5/2 ⇜ (17/6 → 23/8) | note:A5 s:sawtooth gain:0.38051304866630675 attack:0.001 decay:0.2 sustain:0 hcutoff:5614.319554259933 cutoff:4000 ]",
"[ 5/2 ⇜ (17/6 → 23/8) | note:C#5 s:sawtooth gain:0.38051304866630675 attack:0.001 decay:0.2 sustain:0 hcutoff:5614.319554259933 cutoff:4000 ]",
"[ 23/8 → 3/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2386.1887343697626 ]",
"[ 23/8 → 3/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2386.1887343697626 ]",
"[ 23/8 → 3/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2386.1887343697626 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 23/8 → 3/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2386.1887343697626 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (23/8 → 3/1) ⇝ 13/4 | note:A5 s:sawtooth gain:0.38994891982521085 attack:0.001 decay:0.2 sustain:0 hcutoff:5503.134531727652 cutoff:4000 ]",
"[ (23/8 → 3/1) ⇝ 13/4 | note:C#5 s:sawtooth gain:0.38994891982521085 attack:0.001 decay:0.2 sustain:0 hcutoff:5503.134531727652 cutoff:4000 ]",
"[ 21/8 ⇜ (35/12 → 3/1) | note:F#5 s:sawtooth gain:0.38398364517932737 attack:0.001 decay:0.2 sustain:0 hcutoff:5578.674030756363 cutoff:4000 ]",
@ -7913,8 +7913,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (3/1 → 10/3) ⇝ 27/8 | note:E5 s:sawtooth gain:0.39242922708895556 attack:0.001 decay:0.2 sustain:0 hcutoff:5463.2923272018625 cutoff:4000 ]",
"[ 11/4 ⇜ (37/12 → 25/8) | note:A5 s:sawtooth gain:0.3871314633555296 attack:0.001 decay:0.2 sustain:0 hcutoff:5541.603887904197 cutoff:4000 ]",
"[ 11/4 ⇜ (37/12 → 25/8) | note:C#5 s:sawtooth gain:0.3871314633555296 attack:0.001 decay:0.2 sustain:0 hcutoff:5541.603887904197 cutoff:4000 ]",
"[ 25/8 → 13/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2440.8271075661924 ]",
"[ 25/8 → 13/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2440.8271075661924 ]",
"[ 25/8 → 13/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2440.8271075661924 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 25/8 → 13/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2440.8271075661924 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (25/8 → 41/12) ⇝ 7/2 | note:A5 s:sawtooth gain:0.394566409869316 attack:0.001 decay:0.2 sustain:0 hcutoff:5422.104580183649 cutoff:4000 ]",
"[ (25/8 → 41/12) ⇝ 7/2 | note:C#5 s:sawtooth gain:0.394566409869316 attack:0.001 decay:0.2 sustain:0 hcutoff:5422.104580183649 cutoff:4000 ]",
"[ 23/8 ⇜ (19/6 → 13/4) | note:F#5 s:sawtooth gain:0.38994891982521085 attack:0.001 decay:0.2 sustain:0 hcutoff:5503.134531727652 cutoff:4000 ]",
@ -7926,14 +7926,14 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (13/4 → 43/12) ⇝ 29/8 | note:E5 s:sawtooth gain:0.3963553195057793 attack:0.001 decay:0.2 sustain:0 hcutoff:5379.599518697443 cutoff:4000 ]",
"[ 3/1 ⇜ (10/3 → 27/8) | note:A5 s:sawtooth gain:0.39242922708895556 attack:0.001 decay:0.2 sustain:0 hcutoff:5463.2923272018625 cutoff:4000 ]",
"[ 3/1 ⇜ (10/3 → 27/8) | note:C#5 s:sawtooth gain:0.39242922708895556 attack:0.001 decay:0.2 sustain:0 hcutoff:5463.2923272018625 cutoff:4000 ]",
"[ 27/8 → 7/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2493.5603089922215 ]",
"[ 27/8 → 7/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2493.5603089922215 ]",
"[ 27/8 → 7/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2493.5603089922215 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 27/8 → 7/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2493.5603089922215 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (27/8 → 11/3) ⇝ 15/4 | note:A5 s:sawtooth gain:0.3977916463583412 attack:0.001 decay:0.2 sustain:0 hcutoff:5335.806273589214 cutoff:4000 ]",
"[ (27/8 → 11/3) ⇝ 15/4 | note:C#5 s:sawtooth gain:0.3977916463583412 attack:0.001 decay:0.2 sustain:0 hcutoff:5335.806273589214 cutoff:4000 ]",
"[ 25/8 ⇜ (41/12 → 7/2) | note:F#5 s:sawtooth gain:0.394566409869316 attack:0.001 decay:0.2 sustain:0 hcutoff:5422.104580183649 cutoff:4000 ]",
"[ 25/8 ⇜ (41/12 → 7/2) | note:A4 s:sawtooth gain:0.394566409869316 attack:0.001 decay:0.2 sustain:0 hcutoff:5422.104580183649 cutoff:4000 ]",
"[ 7/2 → 29/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2519.1725829012184 ]",
"[ 7/2 → 29/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2519.1725829012184 ]",
"[ 7/2 → 29/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2519.1725829012184 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 7/2 → 29/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2519.1725829012184 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 7/2 → 15/4 | note:F#5 s:sawtooth gain:0.3983764764947172 attack:0.001 decay:0.2 sustain:0 hcutoff:5313.435927530719 cutoff:4000 ]",
"[ 7/2 → 15/4 | note:A4 s:sawtooth gain:0.3983764764947172 attack:0.001 decay:0.2 sustain:0 hcutoff:5313.435927530719 cutoff:4000 ]",
"[ 7/2 → 15/4 | s:bd gain:0.7 ]",
@ -7950,8 +7950,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (29/8 → 4/1) ⇝ 33/8 | note:B3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2568.811347023862 ]",
"[ 27/8 ⇜ (11/3 → 15/4) | note:F#5 s:sawtooth gain:0.3977916463583412 attack:0.001 decay:0.2 sustain:0 hcutoff:5335.806273589214 cutoff:4000 ]",
"[ 27/8 ⇜ (11/3 → 15/4) | note:A4 s:sawtooth gain:0.3977916463583412 attack:0.001 decay:0.2 sustain:0 hcutoff:5335.806273589214 cutoff:4000 ]",
"[ 15/4 → 31/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2568.811347023862 ]",
"[ 15/4 → 31/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2568.811347023862 ]",
"[ 15/4 → 31/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2568.811347023862 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 15/4 → 31/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2568.811347023862 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 15/4 → 4/1 | note:F#5 s:sawtooth gain:0.3998193184307759 attack:0.001 decay:0.2 sustain:0 hcutoff:5220.886439234386 cutoff:4000 ]",
"[ 15/4 → 4/1 | note:A4 s:sawtooth gain:0.3998193184307759 attack:0.001 decay:0.2 sustain:0 hcutoff:5220.886439234386 cutoff:4000 ]",
"[ 15/4 → 4/1 | s:hh3 gain:0.7 ]",
@ -7961,8 +7961,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (15/4 → 4/1) ⇝ 17/4 | note:B3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2580.8797353950404 ]",
"[ 7/2 ⇜ (23/6 → 31/8) | note:A5 s:sawtooth gain:0.3988719301898066 attack:0.001 decay:0.2 sustain:0 hcutoff:5290.754858561636 cutoff:4000 ]",
"[ 7/2 ⇜ (23/6 → 31/8) | note:C#5 s:sawtooth gain:0.3988719301898066 attack:0.001 decay:0.2 sustain:0 hcutoff:5290.754858561636 cutoff:4000 ]",
"[ 31/8 → 4/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2592.8079367021132 ]",
"[ 31/8 → 4/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2592.8079367021132 ]",
"[ 31/8 → 4/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2592.8079367021132 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 31/8 → 4/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2592.8079367021132 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 31/8 → 4/1 | s:bd gain:0.7 ]",
"[ (31/8 → 4/1) ⇝ 17/4 | note:A5 s:sawtooth gain:0.3999548228044306 attack:0.001 decay:0.2 sustain:0 hcutoff:5197.0018638323545 cutoff:4000 ]",
"[ (31/8 → 4/1) ⇝ 17/4 | note:C#5 s:sawtooth gain:0.3999548228044306 attack:0.001 decay:0.2 sustain:0 hcutoff:5197.0018638323545 cutoff:4000 ]",
@ -7987,8 +7987,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ 31/8 ⇜ (4/1 → 35/8) | note:B3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2639.083266757757 ]",
"[ 15/4 ⇜ (49/12 → 33/8) | note:A5 s:sawtooth gain:0.3999548228044306 attack:0.001 decay:0.2 sustain:0 hcutoff:5148.3645377501725 cutoff:4000 ]",
"[ 15/4 ⇜ (49/12 → 33/8) | note:C#5 s:sawtooth gain:0.3999548228044306 attack:0.001 decay:0.2 sustain:0 hcutoff:5148.3645377501725 cutoff:4000 ]",
"[ 33/8 → 17/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2639.083266757757 ]",
"[ 33/8 → 17/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2639.083266757757 ]",
"[ 33/8 → 17/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2639.083266757757 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 33/8 → 17/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2639.083266757757 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (33/8 → 53/12) ⇝ 9/2 | note:A5 s:sawtooth gain:0.3988719301898066 attack:0.001 decay:0.2 sustain:0 hcutoff:5047.734873274585 cutoff:4000 ]",
"[ (33/8 → 53/12) ⇝ 9/2 | note:C#5 s:sawtooth gain:0.3988719301898066 attack:0.001 decay:0.2 sustain:0 hcutoff:5047.734873274585 cutoff:4000 ]",
"[ 31/8 ⇜ (25/6 → 17/4) | note:F#5 s:sawtooth gain:0.3998193184307759 attack:0.001 decay:0.2 sustain:0 hcutoff:5123.62012082546 cutoff:4000 ]",
@ -8000,14 +8000,14 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (17/4 → 55/12) ⇝ 37/8 | note:E5 s:sawtooth gain:0.3977916463583412 attack:0.001 decay:0.2 sustain:0 hcutoff:4995.811501426648 cutoff:4000 ]",
"[ 4/1 ⇜ (13/3 → 35/8) | note:A5 s:sawtooth gain:0.3995935685018036 attack:0.001 decay:0.2 sustain:0 hcutoff:5098.597504951462 cutoff:4000 ]",
"[ 4/1 ⇜ (13/3 → 35/8) | note:C#5 s:sawtooth gain:0.3995935685018036 attack:0.001 decay:0.2 sustain:0 hcutoff:5098.597504951462 cutoff:4000 ]",
"[ 35/8 → 9/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2682.97580859032 ]",
"[ 35/8 → 9/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2682.97580859032 ]",
"[ 35/8 → 9/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2682.97580859032 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 35/8 → 9/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2682.97580859032 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (35/8 → 14/3) ⇝ 19/4 | note:A5 s:sawtooth gain:0.3963553195057793 attack:0.001 decay:0.2 sustain:0 hcutoff:4942.862975093085 cutoff:4000 ]",
"[ (35/8 → 14/3) ⇝ 19/4 | note:C#5 s:sawtooth gain:0.3963553195057793 attack:0.001 decay:0.2 sustain:0 hcutoff:4942.862975093085 cutoff:4000 ]",
"[ 33/8 ⇜ (53/12 → 9/2) | note:F#5 s:sawtooth gain:0.3988719301898066 attack:0.001 decay:0.2 sustain:0 hcutoff:5047.734873274585 cutoff:4000 ]",
"[ 33/8 ⇜ (53/12 → 9/2) | note:A4 s:sawtooth gain:0.3988719301898066 attack:0.001 decay:0.2 sustain:0 hcutoff:5047.734873274585 cutoff:4000 ]",
"[ 9/2 → 37/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2703.995258572327 ]",
"[ 9/2 → 37/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2703.995258572327 ]",
"[ 9/2 → 37/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2703.995258572327 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 9/2 → 37/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2703.995258572327 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 9/2 → 19/4 | note:F#5 s:sawtooth gain:0.3955046879791817 attack:0.001 decay:0.2 sustain:0 hcutoff:4916.015592312082 cutoff:4000 ]",
"[ 9/2 → 19/4 | note:A4 s:sawtooth gain:0.3955046879791817 attack:0.001 decay:0.2 sustain:0 hcutoff:4916.015592312082 cutoff:4000 ]",
"[ 9/2 → 19/4 | s:bd gain:0.7 ]",
@ -8020,8 +8020,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (37/8 → 59/12) ⇝ 5/1 | note:C#5 s:sawtooth gain:0.39242922708895556 attack:0.001 decay:0.2 sustain:0 hcutoff:4834.036289789029 cutoff:4000 ]",
"[ 35/8 ⇜ (14/3 → 19/4) | note:F#5 s:sawtooth gain:0.3963553195057793 attack:0.001 decay:0.2 sustain:0 hcutoff:4942.862975093085 cutoff:4000 ]",
"[ 35/8 ⇜ (14/3 → 19/4) | note:A4 s:sawtooth gain:0.3963553195057793 attack:0.001 decay:0.2 sustain:0 hcutoff:4942.862975093085 cutoff:4000 ]",
"[ 19/4 → 39/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2744.1172176410028 ]",
"[ 19/4 → 39/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2744.1172176410028 ]",
"[ 19/4 → 39/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2744.1172176410028 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 19/4 → 39/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2744.1172176410028 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 19/4 → 5/1 | note:F#5 s:sawtooth gain:0.3912316097774532 attack:0.001 decay:0.2 sustain:0 hcutoff:4806.246411789873 cutoff:4000 ]",
"[ 19/4 → 5/1 | note:A4 s:sawtooth gain:0.3912316097774532 attack:0.001 decay:0.2 sustain:0 hcutoff:4806.246411789873 cutoff:4000 ]",
"[ 19/4 → 5/1 | s:hh3 gain:0.7 ]",
@ -8029,8 +8029,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (19/4 → 5/1) ⇝ 41/8 | note:E5 s:sawtooth gain:0.38994891982521085 attack:0.001 decay:0.2 sustain:0 hcutoff:4778.23271519263 cutoff:4000 ]",
"[ 9/2 ⇜ (29/6 → 39/8) | note:A5 s:sawtooth gain:0.394566409869316 attack:0.001 decay:0.2 sustain:0 hcutoff:4888.925582549005 cutoff:4000 ]",
"[ 9/2 ⇜ (29/6 → 39/8) | note:C#5 s:sawtooth gain:0.394566409869316 attack:0.001 decay:0.2 sustain:0 hcutoff:4888.925582549005 cutoff:4000 ]",
"[ 39/8 → 5/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2763.195558759784 ]",
"[ 39/8 → 5/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2763.195558759784 ]",
"[ 39/8 → 5/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2763.195558759784 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 39/8 → 5/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2763.195558759784 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (39/8 → 5/1) ⇝ 21/4 | note:A5 s:sawtooth gain:0.3871314633555296 attack:0.001 decay:0.2 sustain:0 hcutoff:4721.553103742387 cutoff:4000 ]",
"[ (39/8 → 5/1) ⇝ 21/4 | note:C#5 s:sawtooth gain:0.3871314633555296 attack:0.001 decay:0.2 sustain:0 hcutoff:4721.553103742387 cutoff:4000 ]",
"[ 37/8 ⇜ (59/12 → 5/1) | note:F#5 s:sawtooth gain:0.39242922708895556 attack:0.001 decay:0.2 sustain:0 hcutoff:4834.036289789029 cutoff:4000 ]",
@ -8046,8 +8046,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (5/1 → 16/3) ⇝ 43/8 | note:E5 s:sawtooth gain:0.38398364517932737 attack:0.001 decay:0.2 sustain:0 hcutoff:4664.036300812779 cutoff:4000 ]",
"[ 19/4 ⇜ (61/12 → 41/8) | note:A5 s:sawtooth gain:0.38994891982521085 attack:0.001 decay:0.2 sustain:0 hcutoff:4778.23271519263 cutoff:4000 ]",
"[ 19/4 ⇜ (61/12 → 41/8) | note:C#5 s:sawtooth gain:0.38994891982521085 attack:0.001 decay:0.2 sustain:0 hcutoff:4778.23271519263 cutoff:4000 ]",
"[ 41/8 → 21/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2799.329510692108 ]",
"[ 41/8 → 21/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2799.329510692108 ]",
"[ 41/8 → 21/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2799.329510692108 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 41/8 → 21/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2799.329510692108 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (41/8 → 65/12) ⇝ 11/2 | note:A5 s:sawtooth gain:0.38051304866630675 attack:0.001 decay:0.2 sustain:0 hcutoff:4605.721725547503 cutoff:4000 ]",
"[ (41/8 → 65/12) ⇝ 11/2 | note:C#5 s:sawtooth gain:0.38051304866630675 attack:0.001 decay:0.2 sustain:0 hcutoff:4605.721725547503 cutoff:4000 ]",
"[ 39/8 ⇜ (31/6 → 21/4) | note:F#5 s:sawtooth gain:0.3871314633555296 attack:0.001 decay:0.2 sustain:0 hcutoff:4721.553103742387 cutoff:4000 ]",
@ -8059,14 +8059,14 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (21/4 → 67/12) ⇝ 45/8 | note:E5 s:sawtooth gain:0.3767280347874561 attack:0.001 decay:0.2 sustain:0 hcutoff:4546.64934384357 cutoff:4000 ]",
"[ 5/1 ⇜ (16/3 → 43/8) | note:A5 s:sawtooth gain:0.38398364517932737 attack:0.001 decay:0.2 sustain:0 hcutoff:4664.036300812779 cutoff:4000 ]",
"[ 5/1 ⇜ (16/3 → 43/8) | note:C#5 s:sawtooth gain:0.38398364517932737 attack:0.001 decay:0.2 sustain:0 hcutoff:4664.036300812779 cutoff:4000 ]",
"[ 43/8 → 11/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2832.694627163799 ]",
"[ 43/8 → 11/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2832.694627163799 ]",
"[ 43/8 → 11/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2832.694627163799 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 43/8 → 11/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2832.694627163799 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (43/8 → 17/3) ⇝ 23/4 | note:A5 s:sawtooth gain:0.3726377219727376 attack:0.001 decay:0.2 sustain:0 hcutoff:4486.859640960669 cutoff:4000 ]",
"[ (43/8 → 17/3) ⇝ 23/4 | note:C#5 s:sawtooth gain:0.3726377219727376 attack:0.001 decay:0.2 sustain:0 hcutoff:4486.859640960669 cutoff:4000 ]",
"[ 41/8 ⇜ (65/12 → 11/2) | note:F#5 s:sawtooth gain:0.38051304866630675 attack:0.001 decay:0.2 sustain:0 hcutoff:4605.721725547503 cutoff:4000 ]",
"[ 41/8 ⇜ (65/12 → 11/2) | note:A4 s:sawtooth gain:0.38051304866630675 attack:0.001 decay:0.2 sustain:0 hcutoff:4605.721725547503 cutoff:4000 ]",
"[ 11/2 → 45/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2848.313487543853 ]",
"[ 11/2 → 45/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2848.313487543853 ]",
"[ 11/2 → 45/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2848.313487543853 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 11/2 → 45/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2848.313487543853 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 11/2 → 23/4 | note:F#5 s:sawtooth gain:0.3704811297220968 attack:0.001 decay:0.2 sustain:0 hcutoff:4456.708580912725 cutoff:4000 ]",
"[ 11/2 → 23/4 | note:A4 s:sawtooth gain:0.3704811297220968 attack:0.001 decay:0.2 sustain:0 hcutoff:4456.708580912725 cutoff:4000 ]",
"[ 11/2 → 23/4 | s:bd gain:0.7 ]",
@ -8083,8 +8083,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (45/8 → 6/1) ⇝ 49/8 | note:A3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2877.376777172205 ]",
"[ 43/8 ⇜ (17/3 → 23/4) | note:F#5 s:sawtooth gain:0.3726377219727376 attack:0.001 decay:0.2 sustain:0 hcutoff:4486.859640960669 cutoff:4000 ]",
"[ 43/8 ⇜ (17/3 → 23/4) | note:A4 s:sawtooth gain:0.3726377219727376 attack:0.001 decay:0.2 sustain:0 hcutoff:4486.859640960669 cutoff:4000 ]",
"[ 23/4 → 47/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2877.376777172205 ]",
"[ 23/4 → 47/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2877.376777172205 ]",
"[ 23/4 → 47/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2877.376777172205 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 23/4 → 47/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2877.376777172205 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 23/4 → 6/1 | note:F#5 s:sawtooth gain:0.36114266880324386 attack:0.001 decay:0.2 sustain:0 hcutoff:4334.517148084427 cutoff:4000 ]",
"[ 23/4 → 6/1 | note:A4 s:sawtooth gain:0.36114266880324386 attack:0.001 decay:0.2 sustain:0 hcutoff:4334.517148084427 cutoff:4000 ]",
"[ 23/4 → 6/1 | s:hh3 gain:0.7 ]",
@ -8094,8 +8094,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (23/4 → 6/1) ⇝ 25/4 | note:A3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2884.183170199766 ]",
"[ 11/2 ⇜ (35/6 → 47/8) | note:A5 s:sawtooth gain:0.368251964143991 attack:0.001 decay:0.2 sustain:0 hcutoff:4426.39359377459 cutoff:4000 ]",
"[ 11/2 ⇜ (35/6 → 47/8) | note:C#5 s:sawtooth gain:0.368251964143991 attack:0.001 decay:0.2 sustain:0 hcutoff:4426.39359377459 cutoff:4000 ]",
"[ 47/8 → 6/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2890.803699781578 ]",
"[ 47/8 → 6/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2890.803699781578 ]",
"[ 47/8 → 6/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2890.803699781578 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 47/8 → 6/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2890.803699781578 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (47/8 → 6/1) ⇝ 25/4 | note:A5 s:sawtooth gain:0.3586370624427201 attack:0.001 decay:0.2 sustain:0 hcutoff:4303.598663257904 cutoff:4000 ]",
"[ (47/8 → 6/1) ⇝ 25/4 | note:C#5 s:sawtooth gain:0.3586370624427201 attack:0.001 decay:0.2 sustain:0 hcutoff:4303.598663257904 cutoff:4000 ]",
"[ (47/8 → 6/1) ⇝ 51/8 | note:F#3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2890.803699781578 ]",
@ -8119,8 +8119,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ 47/8 ⇜ (6/1 → 51/8) | note:A3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2915.4076660819765 ]",
"[ 23/4 ⇜ (73/12 → 49/8) | note:A5 s:sawtooth gain:0.35343108171056015 attack:0.001 decay:0.2 sustain:0 hcutoff:4241.3539374389275 cutoff:4000 ]",
"[ 23/4 ⇜ (73/12 → 49/8) | note:C#5 s:sawtooth gain:0.35343108171056015 attack:0.001 decay:0.2 sustain:0 hcutoff:4241.3539374389275 cutoff:4000 ]",
"[ 49/8 → 25/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2915.4076660819765 ]",
"[ 49/8 → 25/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2915.4076660819765 ]",
"[ 49/8 → 25/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2915.4076660819765 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 49/8 → 25/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2915.4076660819765 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (49/8 → 77/12) ⇝ 13/2 | note:A5 s:sawtooth gain:0.3422847385870941 attack:0.001 decay:0.2 sustain:0 hcutoff:4115.383232572483 cutoff:4000 ]",
"[ (49/8 → 77/12) ⇝ 13/2 | note:C#5 s:sawtooth gain:0.3422847385870941 attack:0.001 decay:0.2 sustain:0 hcutoff:4115.383232572483 cutoff:4000 ]",
"[ 47/8 ⇜ (37/6 → 25/4) | note:F#5 s:sawtooth gain:0.3507338432270528 attack:0.001 decay:0.2 sustain:0 hcutoff:4210.038361759807 cutoff:4000 ]",
@ -8132,14 +8132,14 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (25/4 → 79/12) ⇝ 53/8 | note:E5 s:sawtooth gain:0.3363712287126769 attack:0.001 decay:0.2 sustain:0 hcutoff:4051.743587553753 cutoff:4000 ]",
"[ 6/1 ⇜ (19/3 → 51/8) | note:A5 s:sawtooth gain:0.3479759264430665 attack:0.001 decay:0.2 sustain:0 hcutoff:4178.601124662687 cutoff:4000 ]",
"[ 6/1 ⇜ (19/3 → 51/8) | note:C#5 s:sawtooth gain:0.3479759264430665 attack:0.001 decay:0.2 sustain:0 hcutoff:4178.601124662687 cutoff:4000 ]",
"[ 51/8 → 13/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2936.9631544781614 ]",
"[ 51/8 → 13/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2936.9631544781614 ]",
"[ 51/8 → 13/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2936.9631544781614 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 51/8 → 13/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2936.9631544781614 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (51/8 → 20/3) ⇝ 27/4 | note:A5 s:sawtooth gain:0.3302496429830646 attack:0.001 decay:0.2 sustain:0 hcutoff:3987.7258050403216 cutoff:4000 ]",
"[ (51/8 → 20/3) ⇝ 27/4 | note:C#5 s:sawtooth gain:0.3302496429830646 attack:0.001 decay:0.2 sustain:0 hcutoff:3987.7258050403216 cutoff:4000 ]",
"[ 49/8 ⇜ (77/12 → 13/2) | note:F#5 s:sawtooth gain:0.3422847385870941 attack:0.001 decay:0.2 sustain:0 hcutoff:4115.383232572483 cutoff:4000 ]",
"[ 49/8 ⇜ (77/12 → 13/2) | note:A4 s:sawtooth gain:0.3422847385870941 attack:0.001 decay:0.2 sustain:0 hcutoff:4115.383232572483 cutoff:4000 ]",
"[ 13/2 → 53/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2946.5812012110136 ]",
"[ 13/2 → 53/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2946.5812012110136 ]",
"[ 13/2 → 53/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2946.5812012110136 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 13/2 → 53/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2946.5812012110136 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 13/2 → 27/4 | note:F#5 s:sawtooth gain:0.3271154116289833 attack:0.001 decay:0.2 sustain:0 hcutoff:3955.588813730369 cutoff:4000 ]",
"[ 13/2 → 27/4 | note:A4 s:sawtooth gain:0.3271154116289833 attack:0.001 decay:0.2 sustain:0 hcutoff:3955.588813730369 cutoff:4000 ]",
"[ 13/2 → 27/4 | s:bd gain:0.7 ]",
@ -8152,8 +8152,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (53/8 → 83/12) ⇝ 7/1 | note:C#5 s:sawtooth gain:0.3174416994481911 attack:0.001 decay:0.2 sustain:0 hcutoff:3858.7315549779487 cutoff:4000 ]",
"[ 51/8 ⇜ (20/3 → 27/4) | note:F#5 s:sawtooth gain:0.3302496429830646 attack:0.001 decay:0.2 sustain:0 hcutoff:3987.7258050403216 cutoff:4000 ]",
"[ 51/8 ⇜ (20/3 → 27/4) | note:A4 s:sawtooth gain:0.3302496429830646 attack:0.001 decay:0.2 sustain:0 hcutoff:3987.7258050403216 cutoff:4000 ]",
"[ 27/4 → 55/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2963.468935477506 ]",
"[ 27/4 → 55/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2963.468935477506 ]",
"[ 27/4 → 55/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2963.468935477506 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 27/4 → 55/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2963.468935477506 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 27/4 → 7/1 | note:F#5 s:sawtooth gain:0.31413326401454233 attack:0.001 decay:0.2 sustain:0 hcutoff:3826.315480550129 cutoff:4000 ]",
"[ 27/4 → 7/1 | note:A4 s:sawtooth gain:0.31413326401454233 attack:0.001 decay:0.2 sustain:0 hcutoff:3826.315480550129 cutoff:4000 ]",
"[ 27/4 → 7/1 | s:hh3 gain:0.7 ]",
@ -8161,8 +8161,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (27/4 → 7/1) ⇝ 57/8 | note:E5 s:sawtooth gain:0.3107861971007485 attack:0.001 decay:0.2 sustain:0 hcutoff:3793.8434936445938 cutoff:4000 ]",
"[ 13/2 ⇜ (41/6 → 55/8) | note:A5 s:sawtooth gain:0.32393472883446767 attack:0.001 decay:0.2 sustain:0 hcutoff:3923.373759622562 cutoff:4000 ]",
"[ 13/2 ⇜ (41/6 → 55/8) | note:C#5 s:sawtooth gain:0.32393472883446767 attack:0.001 decay:0.2 sustain:0 hcutoff:3923.373759622562 cutoff:4000 ]",
"[ 55/8 → 7/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2970.728450471497 ]",
"[ 55/8 → 7/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2970.728450471497 ]",
"[ 55/8 → 7/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2970.728450471497 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 55/8 → 7/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2970.728450471497 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (55/8 → 7/1) ⇝ 29/4 | note:A5 s:sawtooth gain:0.30398425548024827 attack:0.001 decay:0.2 sustain:0 hcutoff:3728.7540466585065 cutoff:4000 ]",
"[ (55/8 → 7/1) ⇝ 29/4 | note:C#5 s:sawtooth gain:0.30398425548024827 attack:0.001 decay:0.2 sustain:0 hcutoff:3728.7540466585065 cutoff:4000 ]",
"[ 53/8 ⇜ (83/12 → 7/1) | note:F#5 s:sawtooth gain:0.3174416994481911 attack:0.001 decay:0.2 sustain:0 hcutoff:3858.7315549779487 cutoff:4000 ]",
@ -8178,8 +8178,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (7/1 → 22/3) ⇝ 59/8 | note:E5 s:sawtooth gain:0.29705226105983373 attack:0.001 decay:0.2 sustain:0 hcutoff:3663.507823075358 cutoff:4000 ]",
"[ 27/4 ⇜ (85/12 → 57/8) | note:A5 s:sawtooth gain:0.3107861971007485 attack:0.001 decay:0.2 sustain:0 hcutoff:3793.8434936445938 cutoff:4000 ]",
"[ 27/4 ⇜ (85/12 → 57/8) | note:C#5 s:sawtooth gain:0.3107861971007485 attack:0.001 decay:0.2 sustain:0 hcutoff:3793.8434936445938 cutoff:4000 ]",
"[ 57/8 → 29/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2982.856914513109 ]",
"[ 57/8 → 29/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2982.856914513109 ]",
"[ 57/8 → 29/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2982.856914513109 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 57/8 → 29/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2982.856914513109 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (57/8 → 89/12) ⇝ 15/2 | note:A5 s:sawtooth gain:0.29000691362123476 attack:0.001 decay:0.2 sustain:0 hcutoff:3598.149539397671 cutoff:4000 ]",
"[ (57/8 → 89/12) ⇝ 15/2 | note:C#5 s:sawtooth gain:0.29000691362123476 attack:0.001 decay:0.2 sustain:0 hcutoff:3598.149539397671 cutoff:4000 ]",
"[ 55/8 ⇜ (43/6 → 29/4) | note:F#5 s:sawtooth gain:0.30398425548024827 attack:0.001 decay:0.2 sustain:0 hcutoff:3728.7540466585065 cutoff:4000 ]",
@ -8191,14 +8191,14 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (29/4 → 91/12) ⇝ 61/8 | note:E5 s:sawtooth gain:0.28286518602353056 attack:0.001 decay:0.2 sustain:0 hcutoff:3532.7239889283615 cutoff:4000 ]",
"[ 7/1 ⇜ (22/3 → 59/8) | note:A5 s:sawtooth gain:0.29705226105983373 attack:0.001 decay:0.2 sustain:0 hcutoff:3663.507823075358 cutoff:4000 ]",
"[ 7/1 ⇜ (22/3 → 59/8) | note:C#5 s:sawtooth gain:0.29705226105983373 attack:0.001 decay:0.2 sustain:0 hcutoff:3663.507823075358 cutoff:4000 ]",
"[ 59/8 → 15/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2991.774409503181 ]",
"[ 59/8 → 15/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2991.774409503181 ]",
"[ 59/8 → 15/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2991.774409503181 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 59/8 → 15/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2991.774409503181 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (59/8 → 23/3) ⇝ 31/4 | note:A5 s:sawtooth gain:0.2756442833140452 attack:0.001 decay:0.2 sustain:0 hcutoff:3467.276011071639 cutoff:4000 ]",
"[ (59/8 → 23/3) ⇝ 31/4 | note:C#5 s:sawtooth gain:0.2756442833140452 attack:0.001 decay:0.2 sustain:0 hcutoff:3467.276011071639 cutoff:4000 ]",
"[ 57/8 ⇜ (89/12 → 15/2) | note:F#5 s:sawtooth gain:0.29000691362123476 attack:0.001 decay:0.2 sustain:0 hcutoff:3598.149539397671 cutoff:4000 ]",
"[ 57/8 ⇜ (89/12 → 15/2) | note:A4 s:sawtooth gain:0.29000691362123476 attack:0.001 decay:0.2 sustain:0 hcutoff:3598.149539397671 cutoff:4000 ]",
"[ 15/2 → 61/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2995.0220264467503 ]",
"[ 15/2 → 61/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2995.0220264467503 ]",
"[ 15/2 → 61/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2995.0220264467503 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 15/2 → 61/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2995.0220264467503 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 15/2 → 31/4 | note:F#5 s:sawtooth gain:0.2720095711683043 attack:0.001 decay:0.2 sustain:0 hcutoff:3434.557629230318 cutoff:4000 ]",
"[ 15/2 → 31/4 | note:A4 s:sawtooth gain:0.2720095711683043 attack:0.001 decay:0.2 sustain:0 hcutoff:3434.557629230318 cutoff:4000 ]",
"[ 15/2 → 31/4 | s:bd gain:0.7 ]",
@ -8215,8 +8215,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (61/8 → 8/1) ⇝ 65/8 | note:B3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2999.0852191942718 ]",
"[ 59/8 ⇜ (23/3 → 31/4) | note:F#5 s:sawtooth gain:0.2756442833140452 attack:0.001 decay:0.2 sustain:0 hcutoff:3467.276011071639 cutoff:4000 ]",
"[ 59/8 ⇜ (23/3 → 31/4) | note:A4 s:sawtooth gain:0.2756442833140452 attack:0.001 decay:0.2 sustain:0 hcutoff:3467.276011071639 cutoff:4000 ]",
"[ 31/4 → 63/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.0852191942718 ]",
"[ 31/4 → 63/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.0852191942718 ]",
"[ 31/4 → 63/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.0852191942718 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 31/4 → 63/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.0852191942718 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 31/4 → 8/1 | note:F#5 s:sawtooth gain:0.2573601511491127 attack:0.001 decay:0.2 sustain:0 hcutoff:3303.852260680389 cutoff:4000 ]",
"[ 31/4 → 8/1 | note:A4 s:sawtooth gain:0.2573601511491127 attack:0.001 decay:0.2 sustain:0 hcutoff:3303.852260680389 cutoff:4000 ]",
"[ 31/4 → 8/1 | s:hh3 gain:0.7 ]",
@ -8226,8 +8226,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (31/4 → 8/1) ⇝ 33/4 | note:B3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2999.5934052398757 ]",
"[ 15/2 ⇜ (47/6 → 63/8) | note:A5 s:sawtooth gain:0.2683616012798825 attack:0.001 decay:0.2 sustain:0 hcutoff:3401.8504606023293 cutoff:4000 ]",
"[ 15/2 ⇜ (47/6 → 63/8) | note:C#5 s:sawtooth gain:0.2683616012798825 attack:0.001 decay:0.2 sustain:0 hcutoff:3401.8504606023293 cutoff:4000 ]",
"[ 63/8 → 8/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.898347482845 ]",
"[ 63/8 → 8/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.898347482845 ]",
"[ 63/8 → 8/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.898347482845 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 63/8 → 8/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.898347482845 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 63/8 → 8/1 | s:bd gain:0.7 ]",
"[ (63/8 → 8/1) ⇝ 33/4 | note:A5 s:sawtooth gain:0.2536811842784369 attack:0.001 decay:0.2 sustain:0 hcutoff:3271.2459533414954 cutoff:4000 ]",
"[ (63/8 → 8/1) ⇝ 33/4 | note:C#5 s:sawtooth gain:0.2536811842784369 attack:0.001 decay:0.2 sustain:0 hcutoff:3271.2459533414954 cutoff:4000 ]",
@ -8252,8 +8252,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ 63/8 ⇜ (8/1 → 67/8) | note:B3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2999.0852191942718 ]",
"[ 31/4 ⇜ (97/12 → 65/8) | note:A5 s:sawtooth gain:0.24631881572156322 attack:0.001 decay:0.2 sustain:0 hcutoff:3206.156506355406 cutoff:4000 ]",
"[ 31/4 ⇜ (97/12 → 65/8) | note:C#5 s:sawtooth gain:0.24631881572156322 attack:0.001 decay:0.2 sustain:0 hcutoff:3206.156506355406 cutoff:4000 ]",
"[ 65/8 → 33/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.0852191942718 ]",
"[ 65/8 → 33/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.0852191942718 ]",
"[ 65/8 → 33/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.0852191942718 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 65/8 → 33/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2999.0852191942718 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (65/8 → 101/12) ⇝ 17/2 | note:A5 s:sawtooth gain:0.2316383987201176 attack:0.001 decay:0.2 sustain:0 hcutoff:3076.6262403774385 cutoff:4000 ]",
"[ (65/8 → 101/12) ⇝ 17/2 | note:C#5 s:sawtooth gain:0.2316383987201176 attack:0.001 decay:0.2 sustain:0 hcutoff:3076.6262403774385 cutoff:4000 ]",
"[ 63/8 ⇜ (49/6 → 33/4) | note:F#5 s:sawtooth gain:0.24263984885088735 attack:0.001 decay:0.2 sustain:0 hcutoff:3173.6845194498705 cutoff:4000 ]",
@ -8265,14 +8265,14 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (33/4 → 103/12) ⇝ 69/8 | note:E5 s:sawtooth gain:0.2243557166859549 attack:0.001 decay:0.2 sustain:0 hcutoff:3012.274194959679 cutoff:4000 ]",
"[ 8/1 ⇜ (25/3 → 67/8) | note:A5 s:sawtooth gain:0.2389653154600499 attack:0.001 decay:0.2 sustain:0 hcutoff:3141.2684450220513 cutoff:4000 ]",
"[ 8/1 ⇜ (25/3 → 67/8) | note:C#5 s:sawtooth gain:0.2389653154600499 attack:0.001 decay:0.2 sustain:0 hcutoff:3141.2684450220513 cutoff:4000 ]",
"[ 67/8 → 17/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2995.0220264467503 ]",
"[ 67/8 → 17/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2995.0220264467503 ]",
"[ 67/8 → 17/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2995.0220264467503 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 67/8 → 17/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2995.0220264467503 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (67/8 → 26/3) ⇝ 35/4 | note:A5 s:sawtooth gain:0.21713481397646955 attack:0.001 decay:0.2 sustain:0 hcutoff:2948.256412446248 cutoff:4000 ]",
"[ (67/8 → 26/3) ⇝ 35/4 | note:C#5 s:sawtooth gain:0.21713481397646955 attack:0.001 decay:0.2 sustain:0 hcutoff:2948.256412446248 cutoff:4000 ]",
"[ 65/8 ⇜ (101/12 → 17/2) | note:F#5 s:sawtooth gain:0.2316383987201176 attack:0.001 decay:0.2 sustain:0 hcutoff:3076.6262403774385 cutoff:4000 ]",
"[ 65/8 ⇜ (101/12 → 17/2) | note:A4 s:sawtooth gain:0.2316383987201176 attack:0.001 decay:0.2 sustain:0 hcutoff:3076.6262403774385 cutoff:4000 ]",
"[ 17/2 → 69/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2991.774409503181 ]",
"[ 17/2 → 69/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2991.774409503181 ]",
"[ 17/2 → 69/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2991.774409503181 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 17/2 → 69/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2991.774409503181 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 17/2 → 35/4 | note:F#5 s:sawtooth gain:0.21355297301451046 attack:0.001 decay:0.2 sustain:0 hcutoff:2916.386590360237 cutoff:4000 ]",
"[ 17/2 → 35/4 | note:A4 s:sawtooth gain:0.21355297301451046 attack:0.001 decay:0.2 sustain:0 hcutoff:2916.386590360237 cutoff:4000 ]",
"[ 17/2 → 35/4 | s:bd gain:0.7 ]",
@ -8285,8 +8285,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (69/8 → 107/12) ⇝ 9/1 | note:C#5 s:sawtooth gain:0.20294773894016632 attack:0.001 decay:0.2 sustain:0 hcutoff:2821.398875337315 cutoff:4000 ]",
"[ 67/8 ⇜ (26/3 → 35/4) | note:F#5 s:sawtooth gain:0.21713481397646955 attack:0.001 decay:0.2 sustain:0 hcutoff:2948.256412446248 cutoff:4000 ]",
"[ 67/8 ⇜ (26/3 → 35/4) | note:A4 s:sawtooth gain:0.21713481397646955 attack:0.001 decay:0.2 sustain:0 hcutoff:2948.256412446248 cutoff:4000 ]",
"[ 35/4 → 71/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2982.856914513109 ]",
"[ 35/4 → 71/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2982.856914513109 ]",
"[ 35/4 → 71/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2982.856914513109 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 35/4 → 71/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2982.856914513109 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 35/4 → 9/1 | note:F#5 s:sawtooth gain:0.19946652199116702 attack:0.001 decay:0.2 sustain:0 hcutoff:2789.9616382401937 cutoff:4000 ]",
"[ 35/4 → 9/1 | note:A4 s:sawtooth gain:0.19946652199116702 attack:0.001 decay:0.2 sustain:0 hcutoff:2789.9616382401937 cutoff:4000 ]",
"[ 35/4 → 9/1 | s:hh3 gain:0.7 ]",
@ -8294,8 +8294,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (35/4 → 9/1) ⇝ 73/8 | note:E5 s:sawtooth gain:0.1960157445197518 attack:0.001 decay:0.2 sustain:0 hcutoff:2758.6460625610725 cutoff:4000 ]",
"[ 17/2 ⇜ (53/6 → 71/8) | note:A5 s:sawtooth gain:0.2099930863787653 attack:0.001 decay:0.2 sustain:0 hcutoff:2884.6167674275184 cutoff:4000 ]",
"[ 17/2 ⇜ (53/6 → 71/8) | note:C#5 s:sawtooth gain:0.2099930863787653 attack:0.001 decay:0.2 sustain:0 hcutoff:2884.6167674275184 cutoff:4000 ]",
"[ 71/8 → 9/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2977.1924080321423 ]",
"[ 71/8 → 9/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2977.1924080321423 ]",
"[ 71/8 → 9/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2977.1924080321423 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 71/8 → 9/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2977.1924080321423 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (71/8 → 9/1) ⇝ 37/4 | note:A5 s:sawtooth gain:0.18921380289925155 attack:0.001 decay:0.2 sustain:0 hcutoff:2696.4013367420957 cutoff:4000 ]",
"[ (71/8 → 9/1) ⇝ 37/4 | note:C#5 s:sawtooth gain:0.18921380289925155 attack:0.001 decay:0.2 sustain:0 hcutoff:2696.4013367420957 cutoff:4000 ]",
"[ 69/8 ⇜ (107/12 → 9/1) | note:F#5 s:sawtooth gain:0.20294773894016632 attack:0.001 decay:0.2 sustain:0 hcutoff:2821.398875337315 cutoff:4000 ]",
@ -8311,8 +8311,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (9/1 → 28/3) ⇝ 75/8 | note:E5 s:sawtooth gain:0.182558300551809 attack:0.001 decay:0.2 sustain:0 hcutoff:2634.707357306267 cutoff:4000 ]",
"[ 35/4 ⇜ (109/12 → 73/8) | note:A5 s:sawtooth gain:0.1960157445197518 attack:0.001 decay:0.2 sustain:0 hcutoff:2758.6460625610725 cutoff:4000 ]",
"[ 35/4 ⇜ (109/12 → 73/8) | note:C#5 s:sawtooth gain:0.1960157445197518 attack:0.001 decay:0.2 sustain:0 hcutoff:2758.6460625610725 cutoff:4000 ]",
"[ 73/8 → 37/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2963.4689354775064 ]",
"[ 73/8 → 37/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2963.4689354775064 ]",
"[ 73/8 → 37/4 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2963.4689354775064 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 73/8 → 37/4 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2963.4689354775064 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (73/8 → 113/12) ⇝ 19/2 | note:A5 s:sawtooth gain:0.17606527116553244 attack:0.001 decay:0.2 sustain:0 hcutoff:2573.60640622541 cutoff:4000 ]",
"[ (73/8 → 113/12) ⇝ 19/2 | note:C#5 s:sawtooth gain:0.17606527116553244 attack:0.001 decay:0.2 sustain:0 hcutoff:2573.60640622541 cutoff:4000 ]",
"[ 71/8 ⇜ (55/6 → 37/4) | note:F#5 s:sawtooth gain:0.18921380289925155 attack:0.001 decay:0.2 sustain:0 hcutoff:2696.4013367420957 cutoff:4000 ]",
@ -8324,14 +8324,14 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (37/4 → 115/12) ⇝ 77/8 | note:E5 s:sawtooth gain:0.16975035701693547 attack:0.001 decay:0.2 sustain:0 hcutoff:2513.140359039332 cutoff:4000 ]",
"[ 9/1 ⇜ (28/3 → 75/8) | note:A5 s:sawtooth gain:0.182558300551809 attack:0.001 decay:0.2 sustain:0 hcutoff:2634.707357306267 cutoff:4000 ]",
"[ 9/1 ⇜ (28/3 → 75/8) | note:C#5 s:sawtooth gain:0.182558300551809 attack:0.001 decay:0.2 sustain:0 hcutoff:2634.707357306267 cutoff:4000 ]",
"[ 75/8 → 19/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2946.5812012110136 ]",
"[ 75/8 → 19/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2946.5812012110136 ]",
"[ 75/8 → 19/2 | note:D2 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2946.5812012110136 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 75/8 → 19/2 | note:D2 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2946.5812012110136 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (75/8 → 29/3) ⇝ 39/4 | note:A5 s:sawtooth gain:0.16362877128732323 attack:0.001 decay:0.2 sustain:0 hcutoff:2453.350656156431 cutoff:4000 ]",
"[ (75/8 → 29/3) ⇝ 39/4 | note:C#5 s:sawtooth gain:0.16362877128732323 attack:0.001 decay:0.2 sustain:0 hcutoff:2453.350656156431 cutoff:4000 ]",
"[ 73/8 ⇜ (113/12 → 19/2) | note:F#5 s:sawtooth gain:0.17606527116553244 attack:0.001 decay:0.2 sustain:0 hcutoff:2573.60640622541 cutoff:4000 ]",
"[ 73/8 ⇜ (113/12 → 19/2) | note:A4 s:sawtooth gain:0.17606527116553244 attack:0.001 decay:0.2 sustain:0 hcutoff:2573.60640622541 cutoff:4000 ]",
"[ 19/2 → 77/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2936.9631544781614 ]",
"[ 19/2 → 77/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2936.9631544781614 ]",
"[ 19/2 → 77/8 | note:D1 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2936.9631544781614 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 19/2 → 77/8 | note:D1 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2936.9631544781614 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 19/2 → 39/4 | note:F#5 s:sawtooth gain:0.16064510432613502 attack:0.001 decay:0.2 sustain:0 hcutoff:2423.7222579792624 cutoff:4000 ]",
"[ 19/2 → 39/4 | note:A4 s:sawtooth gain:0.16064510432613502 attack:0.001 decay:0.2 sustain:0 hcutoff:2423.7222579792624 cutoff:4000 ]",
"[ 19/2 → 39/4 | s:bd gain:0.7 ]",
@ -8348,8 +8348,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (77/8 → 10/1) ⇝ 81/8 | note:A3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2915.4076660819765 ]",
"[ 75/8 ⇜ (29/3 → 39/4) | note:F#5 s:sawtooth gain:0.16362877128732323 attack:0.001 decay:0.2 sustain:0 hcutoff:2453.350656156431 cutoff:4000 ]",
"[ 75/8 ⇜ (29/3 → 39/4) | note:A4 s:sawtooth gain:0.16362877128732323 attack:0.001 decay:0.2 sustain:0 hcutoff:2453.350656156431 cutoff:4000 ]",
"[ 39/4 → 79/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2915.4076660819765 ]",
"[ 39/4 → 79/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2915.4076660819765 ]",
"[ 39/4 → 79/8 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2915.4076660819765 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 39/4 → 79/8 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2915.4076660819765 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 39/4 → 10/1 | note:F#5 s:sawtooth gain:0.14926615677294724 attack:0.001 decay:0.2 sustain:0 hcutoff:2307.1030993509794 cutoff:4000 ]",
"[ 39/4 → 10/1 | note:A4 s:sawtooth gain:0.14926615677294724 attack:0.001 decay:0.2 sustain:0 hcutoff:2307.1030993509794 cutoff:4000 ]",
"[ 39/4 → 10/1 | s:hh3 gain:0.7 ]",
@ -8359,8 +8359,8 @@ exports[`renders tunes > tune: hyperpop 1`] = `
"[ (39/4 → 10/1) ⇝ 41/4 | note:A3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2909.5402784268977 ]",
"[ 19/2 ⇜ (59/6 → 79/8) | note:A5 s:sawtooth gain:0.157715261412906 attack:0.001 decay:0.2 sustain:0 hcutoff:2394.2782744524975 cutoff:4000 ]",
"[ 19/2 ⇜ (59/6 → 79/8) | note:C#5 s:sawtooth gain:0.157715261412906 attack:0.001 decay:0.2 sustain:0 hcutoff:2394.2782744524975 cutoff:4000 ]",
"[ 79/8 → 10/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2903.483208638841 ]",
"[ 79/8 → 10/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2903.483208638841 ]",
"[ 79/8 → 10/1 | note:D3 s:sawtooth gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2903.483208638841 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ 79/8 → 10/1 | note:D3 s:square gain:0.3 attack:0.01 decay:0.1 sustain:0.5 cutoff:2903.483208638841 lpattack:0.1 lpenv:2 ftype:24db ]",
"[ (79/8 → 10/1) ⇝ 41/4 | note:A5 s:sawtooth gain:0.14656891828944 attack:0.001 decay:0.2 sustain:0 hcutoff:2278.446896257612 cutoff:4000 ]",
"[ (79/8 → 10/1) ⇝ 41/4 | note:C#5 s:sawtooth gain:0.14656891828944 attack:0.001 decay:0.2 sustain:0 hcutoff:2278.446896257612 cutoff:4000 ]",
"[ (79/8 → 10/1) ⇝ 83/8 | note:F#3 s:square gain:0.7 attack:0.01 decay:0.1 sustain:0 cutoff:2903.483208638841 ]",
@ -8372,16 +8372,16 @@ exports[`renders tunes > tune: hyperpop 1`] = `
exports[`renders tunes > tune: juxUndTollerei 1`] = `
[
"[ 0/1 → 1/4 | note:bb3 s:sawtooth pan:0 cutoff:1188.2154262966046 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:1 cutoff:1188.2154262966046 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:0 cutoff:1361.2562095290161 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:1 cutoff:1361.2562095290161 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/2 → 3/4 | note:eb3 s:sawtooth pan:0 cutoff:1524.257063143398 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:1 cutoff:1524.257063143398 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ (101/200 → 1/1) ⇝ 201/200 | note:65 s:triangle pan:0 cutoff:1601.4815730092653 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ (101/200 → 1/1) ⇝ 201/200 | note:55 s:triangle pan:1 cutoff:1601.4815730092653 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 3/4 → 1/1 | note:c3 s:sawtooth pan:0 cutoff:1670.953955747281 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 3/4 → 1/1 | note:bb3 s:sawtooth pan:1 cutoff:1670.953955747281 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:0 cutoff:1188.2154262966046 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:1 cutoff:1188.2154262966046 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:0 cutoff:1361.2562095290161 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/4 → 1/2 | note:eb3 s:sawtooth pan:1 cutoff:1361.2562095290161 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/2 → 3/4 | note:eb3 s:sawtooth pan:0 cutoff:1524.257063143398 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/2 → 3/4 | note:g3 s:sawtooth pan:1 cutoff:1524.257063143398 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ (101/200 → 1/1) ⇝ 201/200 | note:65 s:triangle pan:0 cutoff:1601.4815730092653 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ (101/200 → 1/1) ⇝ 201/200 | note:55 s:triangle pan:1 cutoff:1601.4815730092653 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 3/4 → 1/1 | note:c3 s:sawtooth pan:0 cutoff:1670.953955747281 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 3/4 → 1/1 | note:bb3 s:sawtooth pan:1 cutoff:1670.953955747281 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
]
`;
@ -8422,8 +8422,8 @@ exports[`renders tunes > tune: meltingsubmarine 1`] = `
"[ 0/1 → 3/16 | note:93.00057728554401 decay:0.1 sustain:0 s:triangle gain:0.075 ]",
"[ 0/1 → 3/16 | note:93.04057728554402 decay:0.1 sustain:0 s:triangle gain:0.075 ]",
"[ (0/1 → 1/1) ⇝ 3/2 | s:bd n:5 speed:0.7519542165100574 ]",
"[ (0/1 → 1/1) ⇝ 3/2 | note:33.129885541275144 decay:0.15 sustain:0 s:sawtooth gain:0.4 cutoff:3669.6267869262615 ]",
"[ (0/1 → 1/1) ⇝ 3/2 | note:33.17988554127514 decay:0.15 sustain:0 s:sawtooth gain:0.4 cutoff:3669.6267869262615 ]",
"[ (0/1 → 1/1) ⇝ 3/2 | note:33.129885541275144 decay:0.15 sustain:0 s:sawtooth gain:0.4 cutoff:3669.6267869262615 lpattack:0.1 lpenv:-2 ]",
"[ (0/1 → 1/1) ⇝ 3/2 | note:33.17988554127514 decay:0.15 sustain:0 s:sawtooth gain:0.4 cutoff:3669.6267869262615 lpattack:0.1 lpenv:-2 ]",
"[ (0/1 → 1/1) ⇝ 3/2 | note:60.129885541275144 s:sawtooth gain:0.16 cutoff:500 attack:1 ]",
"[ (0/1 → 1/1) ⇝ 3/2 | note:60.16988554127514 s:sawtooth gain:0.16 cutoff:500 attack:1 ]",
"[ (0/1 → 1/1) ⇝ 3/2 | note:64.12988554127514 s:sawtooth gain:0.16 cutoff:500 attack:1 ]",
@ -8481,21 +8481,6 @@ exports[`renders tunes > tune: orbit 1`] = `
]
`;
exports[`renders tunes > tune: outroMusic 1`] = `
[
"[ 0/1 → 1/2 | s:hh speed:0.9036881079621337 n:3 ]",
"[ 0/1 → 3/4 | n:-7 note:C2 s:sawtooth attack:0.05 decay:0.1 sustain:0.7 cutoff:864.536878321087 gain:0.3 ]",
"[ 0/1 → 3/4 | s:bd speed:0.9107561463868479 n:3 ]",
"[ (0/1 → 1/1) ⇝ 3/1 | note:B3 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 3/1 | note:D4 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 3/1 | note:E4 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 3/1 | note:G4 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ (0/1 → 1/1) ⇝ 9/2 | n:1 note:C5 s:gm_epiano1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
"[ 1/2 → 1/1 | s:hh speed:0.9519542165100575 n:3 ]",
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd speed:0.9931522866332672 n:3 ]",
]
`;
exports[`renders tunes > tune: randomBells 1`] = `
[
"[ -9/8 ⇜ (0/1 → 3/8) | note:G4 s:bell gain:0.6 delay:0.2 delaytime:0.3333333333333333 delayfeedback:0.8 ]",
@ -10180,22 +10165,22 @@ exports[`renders tunes > tune: undergroundPlumber 1`] = `
exports[`renders tunes > tune: waa2 1`] = `
[
"[ -1/4 ⇜ (0/1 → 1/4) | n:48 clip:1.1738393178344886 s:sawtooth cutoff:3997.892048359052 gain:0.5 room:0.5 ]",
"[ -1/4 ⇜ (0/1 → 1/4) | n:64 clip:1.1738393178344886 s:sawtooth cutoff:3997.892048359052 gain:0.5 room:0.5 ]",
"[ (0/1 → 1/4) ⇝ 1/2 | n:62 clip:1.197659880151613 s:sawtooth cutoff:3991.5732716763446 gain:0.5 room:0.5 ]",
"[ (0/1 → 1/4) ⇝ 1/2 | n:43 clip:1.197659880151613 s:sawtooth cutoff:3991.5732716763446 gain:0.5 room:0.5 ]",
"[ 0/1 ⇜ (1/4 → 1/2) | n:62 clip:1.197659880151613 s:square cutoff:3991.5732716763446 gain:0.5 room:0.5 ]",
"[ 0/1 ⇜ (1/4 → 1/2) | n:43 clip:1.197659880151613 s:square cutoff:3991.5732716763446 gain:0.5 room:0.5 ]",
"[ (1/4 → 1/2) ⇝ 3/4 | n:74 clip:1.2451698046878117 s:square cutoff:3966.3742407056534 gain:0.5 room:0.5 ]",
"[ (1/4 → 1/2) ⇝ 3/4 | n:55 clip:1.2451698046878117 s:square cutoff:3966.3742407056534 gain:0.5 room:0.5 ]",
"[ 1/4 ⇜ (1/2 → 3/4) | n:74 clip:1.2451698046878117 s:sawtooth cutoff:3966.3742407056534 gain:0.5 room:0.5 ]",
"[ 1/4 ⇜ (1/2 → 3/4) | n:55 clip:1.2451698046878117 s:sawtooth cutoff:3966.3742407056534 gain:0.5 room:0.5 ]",
"[ 1/2 → 3/4 | n:50 clip:1.2688217886051745 s:sawtooth cutoff:3947.554693090452 gain:0.5 room:0.5 ]",
"[ (1/2 → 3/4) ⇝ 1/1 | n:69 clip:1.292380289809026 s:sawtooth cutoff:3924.645587531366 gain:0.5 room:0.5 ]",
"[ 1/2 ⇜ (3/4 → 1/1) | n:69 clip:1.292380289809026 s:square cutoff:3924.645587531366 gain:0.5 room:0.5 ]",
"[ 3/4 → 1/1 | n:41 clip:1.315826773713709 s:square cutoff:3897.7021140702864 gain:0.5 room:0.5 ]",
"[ 3/4 → 1/1 | n:62 clip:1.315826773713709 s:square cutoff:3897.7021140702864 gain:0.5 room:0.5 ]",
"[ (3/4 → 1/1) ⇝ 5/4 | n:81 clip:1.315826773713709 s:square cutoff:3897.7021140702864 gain:0.5 room:0.5 ]",
"[ -1/4 ⇜ (0/1 → 1/4) | note:48 clip:1.1738393178344886 s:sawtooth cutoff:3997.892048359052 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ -1/4 ⇜ (0/1 → 1/4) | note:64 clip:1.1738393178344886 s:sawtooth cutoff:3997.892048359052 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ (0/1 → 1/4) ⇝ 1/2 | note:62 clip:1.197659880151613 s:sawtooth cutoff:3991.5732716763446 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ (0/1 → 1/4) ⇝ 1/2 | note:43 clip:1.197659880151613 s:sawtooth cutoff:3991.5732716763446 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ 0/1 ⇜ (1/4 → 1/2) | note:62 clip:1.197659880151613 s:square cutoff:3991.5732716763446 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ 0/1 ⇜ (1/4 → 1/2) | note:43 clip:1.197659880151613 s:square cutoff:3991.5732716763446 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ (1/4 → 1/2) ⇝ 3/4 | note:74 clip:1.2451698046878117 s:square cutoff:3966.3742407056534 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ (1/4 → 1/2) ⇝ 3/4 | note:55 clip:1.2451698046878117 s:square cutoff:3966.3742407056534 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ 1/4 ⇜ (1/2 → 3/4) | note:74 clip:1.2451698046878117 s:sawtooth cutoff:3966.3742407056534 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ 1/4 ⇜ (1/2 → 3/4) | note:55 clip:1.2451698046878117 s:sawtooth cutoff:3966.3742407056534 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ 1/2 → 3/4 | note:50 clip:1.2688217886051745 s:sawtooth cutoff:3947.554693090452 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ (1/2 → 3/4) ⇝ 1/1 | note:69 clip:1.292380289809026 s:sawtooth cutoff:3924.645587531366 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ 1/2 ⇜ (3/4 → 1/1) | note:69 clip:1.292380289809026 s:square cutoff:3924.645587531366 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ 3/4 → 1/1 | note:41 clip:1.315826773713709 s:square cutoff:3897.7021140702864 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ 3/4 → 1/1 | note:62 clip:1.315826773713709 s:square cutoff:3897.7021140702864 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
"[ (3/4 → 1/1) ⇝ 5/4 | note:81 clip:1.315826773713709 s:square cutoff:3897.7021140702864 gain:0.5 room:0.5 lpattack:0.125 lpenv:-2 vib:8 vibmod:0.125 fanchor:0.25 ]",
]
`;

View File

@ -34,9 +34,11 @@
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*",
"@strudel/desktopbridge": "workspace:*",
"@supabase/supabase-js": "^2.21.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@tauri-apps/api": "^1.4.0",
"@types/node": "^18.16.3",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",

Binary file not shown.

View File

@ -0,0 +1 @@
This super cool font is by 3d@galax.xyz https://galax.xyz/TELETEXT/

Binary file not shown.

View File

@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

View File

@ -46,12 +46,14 @@ const base = BASE_URL;
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
<script>
import { settings } from '../repl/themes.mjs';
import { settings, injectStyle } from '../repl/themes.mjs';
import { settingsMap } from '../settings.mjs';
import { listenKeys } from 'nanostores';
const themeStyle = document.createElement('style');
themeStyle.id = 'strudel-theme';
document.head.append(themeStyle);
let resetThemeStyle;
function activateTheme(name) {
if (!settings[name]) {
console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings');
@ -70,6 +72,11 @@ const base = BASE_URL;
} else {
document.documentElement.classList.add('dark');
}
resetThemeStyle?.();
resetThemeStyle = undefined;
if (themeSettings.customStyle) {
resetThemeStyle = injectStyle(themeSettings.customStyle);
}
}
activateTheme(settingsMap.get().theme);

View File

@ -12,10 +12,11 @@ export function JsDoc({ name, h = 3, hideDescription, punchcard, canvasHeight })
}
const synonyms = getTag('synonyms', item)?.split(', ') || [];
const CustomHeading = `h${h}`;
const description = item.description.replaceAll(/\{@link ([a-zA-Z\.]+)?#?([a-zA-Z]*)\}/g, (_, a, b) => {
// console.log(_, 'a', a, 'b', b);
return `<a href="#${a.replaceAll('.', '').toLowerCase()}${b ? `-${b}` : ''}">${a}${b ? `#${b}` : ''}</a>`;
});
const description =
item.description?.replaceAll(/\{@link ([a-zA-Z\.]+)?#?([a-zA-Z]*)\}/g, (_, a, b) => {
// console.log(_, 'a', a, 'b', b);
return `<a href="#${a.replaceAll('.', '').toLowerCase()}${b ? `-${b}` : ''}">${a}${b ? `#${b}` : ''}</a>`;
}) || '';
return (
<>
{!!h && <CustomHeading>{item.longname}</CustomHeading>}

View File

@ -49,6 +49,10 @@ Each filter has 2 parameters:
<JsDoc client:idle name="bpq" h={0} />
## ftype
<JsDoc client:idle name="ftype" h={0} />
## vowel
<JsDoc client:idle name="vowel" h={0} />
@ -78,6 +82,58 @@ Strudel uses ADSR envelopes, which are probably the most common way to describe
<JsDoc client:idle name="release" h={0} />
# Filter Envelope
Each filter can receive an additional filter envelope controlling the cutoff value dynamically. It uses an ADSR envelope similar to the one used for amplitude. There is an additional parameter to control the depth of the filter modulation: `lpenv`|`hpenv`|`bpenv`. This allows you to play subtle or huge filter modulations just the same by only increasing or decreasing the depth.
<MiniRepl
client:idle
tune={`note("[c eb g <f bb>](3,8,<0 1>)".sub(12))
.s("<sawtooth>/64")
.lpf(sine.range(500,3000).slow(16))
.lpa(0.005)
.lpd(perlin.range(.02,.2))
.lps(perlin.range(0,.5).slow(3))
.lpq(sine.range(2,10).slow(32))
.release(.5)
.lpenv(perlin.range(1,8).slow(2))
.ftype('24db')
.room(1)
.juxBy(.5,rev)
.sometimes(add(note(12)))
.stack(s("bd*2").bank('RolandTR909'))
.gain(.5)`}
/>
There is one filter envelope for each filter type and thus one set of envelope filter parameters preceded either by `lp`, `hp` or `bp`:
- `lpattack`, `lpdecay`, `lpsustain`, `lprelease`, `lpenv`: filter envelope for the lowpass filter.
- alternatively: `lpa`, `lpd`, `lps`, `lpr` and `lpe`.
- `hpattack`, `hpdecay`, `hpsustain`, `hprelease`, `hpenv`: filter envelope for the highpass filter.
- alternatively: `hpa`, `hpd`, `hps`, `hpr` and `hpe`.
- `bpattack`, `bpdecay`, `bpsustain`, `bprelease`, `bpenv`: filter envelope for the bandpass filter.
- alternatively: `bpa`, `bpd`, `bps`, `bpr` and `bpe`.
## lpattack
<JsDoc client:idle name="lpattack" h={0} />
## lpdecay
<JsDoc client:idle name="lpdecay" h={0} />
## lpsustain
<JsDoc client:idle name="lpsustain" h={0} />
## lprelease
<JsDoc client:idle name="lprelease" h={0} />
## lpenv
<JsDoc client:idle name="lpenv" h={0} />
# Dynamics
## gain

View File

@ -303,6 +303,18 @@ Sampler effects are functions that can be used to change the behaviour of sample
<JsDoc client:idle name="Pattern.end" h={0} />
### loop
<JsDoc client:idle name="loop" h={0} />
### loopBegin
<JsDoc client:idle name="loopBegin" h={0} />
### loopEnd
<JsDoc client:idle name="loopEnd" h={0} />
### cut
<JsDoc client:idle name="cut" h={0} />
@ -315,6 +327,10 @@ Sampler effects are functions that can be used to change the behaviour of sample
<JsDoc client:idle name="Pattern.loopAt" h={0} />
### fit
<JsDoc client:idle name="fit" h={0} />
### chop
<JsDoc client:idle name="Pattern.chop" h={0} />
@ -323,6 +339,10 @@ Sampler effects are functions that can be used to change the behaviour of sample
<JsDoc client:idle name="Pattern.slice" h={0} />
### splice
<JsDoc client:idle name="splice" h={0} />
### speed
<JsDoc client:idle name="speed" h={0} />

View File

@ -8,28 +8,62 @@ import { JsDoc } from '../../docs/JsDoc';
# Synths
For now, [samples](/learn/samples) are the main way to play with Strudel.
In the future, more powerful synthesis capabilities will be added.
If in the meantime you want to dive deeper into audio synthesis with Tidal, you will need to [install SuperCollider and SuperDirt](https://tidalcycles.org/docs/).
In addition to the sampling engine, strudel comes with a synthesizer to create sounds on the fly.
## Playing synths with `s`
## Basic Waveforms
We can change the sound, using the `s` function:
The basic waveforms are `sine`, `sawtooth`, `square` and `triangle`, which can be selected via `sound` (or `s`):
<MiniRepl client:idle tune={`note("c2 <eb2 <g2 g1>>").s('sawtooth')`} />
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>")
.sound("<sawtooth square triangle sine>")
.scope()`}
/>
Here, we are wrapping our notes inside `note` and set the sound using `s`, connected by a dot.
If you don't set a `sound` but a `note` the default value for `sound` is `triangle`!
Those functions are only 2 of many ways to alter the properties, or _params_ of a sound.
The power of patterns allows us to sequence any _param_ independently:
### Additive Synthesis
<MiniRepl client:idle tune={`note("c2 <eb2 <g2 g1>>").s("<sawtooth square triangle>")`} />
To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform:
Now we not only pattern the notes, but the sound as well!
`sawtooth` `square` and `triangle` are the basic waveforms available in `s`.
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>")
.sound("sawtooth")
.n("<32 16 8 4>")
.scope()`}
/>
When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting.
You can also set `n` directly in mini notation with `sound`:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>")
.sound("sawtooth:<32 16 8 4>")
.scope()`}
/>
Note for tidal users: `n` in tidal is synonymous to `note` for synths only.
In strudel, this is not the case, where `n` will always change timbre, be it though different samples or different waveforms.
## Vibrato
### vib
<JsDoc client:idle name="vib" h={0} />
### vibmod
<JsDoc client:idle name="vibmod" h={0} />
## FM Synthesis
FM Synthesis is a technique that changes the frequency of a basic waveform rapidly to alter the timbre.
You can use fm with any of the above waveforms, although the below examples all use the default triangle wave.
### fm
<JsDoc client:idle name="fm" h={0} />
@ -38,4 +72,74 @@ Now we not only pattern the notes, but the sound as well!
<JsDoc client:idle name="fmh" h={0} />
### fmattack
<JsDoc client:idle name="fmattack" h={0} />
### fmdecay
<JsDoc client:idle name="fmdecay" h={0} />
### fmsustain
<JsDoc client:idle name="fmsustain" h={0} />
### fmenv
<JsDoc client:idle name="fmenv" h={0} />
## Wavetable Synthesis
Strudel can also use the sampler to load custom waveforms as a replacement of the default waveforms used by WebAudio for the base synth. A default set of more than 1000 wavetables is accessible by default (coming from the [AKWF](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) set). You can also import/use your own. A wavetable is a one-cycle waveform, which is then repeated to create a sound at the desired frequency. It is a classic but very effective synthesis technique.
Any sample preceded by the `wt_` prefix will be loaded as a wavetable. This means that the `loop` argument will be set to `1` by defalt. You can scan over the wavetable by using `loopBegin` and `loopEnd` as well.
<MiniRepl
client:idle
tune={`samples('github:Bubobubobubobubo/Dough-Waveforms/main/');
note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")
.n("<1 2 3 4 5 6 7 8 9 10>/2").room(0.5).size(0.9)
.s('wt_flute').velocity(0.25).often(n => n.ply(2))
.release(0.125).decay("<0.1 0.25 0.3 0.4>").sustain(0)
.cutoff(2000).scope({}).cutoff("<1000 2000 4000>").fast(2)`}
/>
## ZZFX
The "Zuper Zmall Zound Zynth" [ZZFX](https://github.com/KilledByAPixel/ZzFX) is also integrated in strudel.
Developed by [Frank Force](https://frankforce.com/), it is a synth and FX engine originally intended to be used for size coding games.
It has 20 parameters in total, here is a snippet that uses all:
<MiniRepl
client:idle
tune={`note("<c2 eb2 f2 g2>") // also supports freq
.s("<z_sawtooth z_tan z_noise z_sine z_square>")
.zrand(0) // randomization
// zzfx envelope
.attack(0.001)
.decay(0.1)
.sustain(.8)
.release(.1)
// special zzfx params
.curve(1) // waveshape 1-3
.slide(0) // +/- pitch slide
.deltaSlide(0) // +/- pitch slide (?)
.noise(0) // make it dirty
.zmod(0) // fm speed
.zcrush(0) // bit crush 0 - 1
.zdelay(0) // simple delay
.pitchJump(0) // +/- pitch change after pitchJumpTime
.pitchJumpTime(0) // >0 time after pitchJump is applied
.lfo(0) // >0 resets slide + pitchJump + sets tremolo speed
.tremolo(0) // 0-1 lfo volume modulation amount
//.duration(.2) // overwrite strudel event duration
//.gain(1) // change volume
.scope() // vizualise waveform (not zzfx related)
`}
/>
Note that you can also combine zzfx with all the other audio fx (next chapter).
Next up: [Audio Effects](/learn/effects)...

View File

@ -53,3 +53,7 @@
#code .cm-cursor {
border-left: 2px solid currentcolor !important;
}
#code .cm-foldGutter {
display: none !important;
}

View File

@ -5,15 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
import {
CodeMirror,
cx,
flash,
useHighlighting,
useStrudel,
useKeydown,
updateMiniLocations,
} from '@strudel.cycles/react';
import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react';
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
import { createClient } from '@supabase/supabase-js';
import { nanoid } from 'nanoid';
@ -28,6 +20,8 @@ import { themes } from './themes.mjs';
import { settingsMap, useSettings, setLatestCode } from '../settings.mjs';
import Loader from './Loader';
import { settingPatterns } from '../settings.mjs';
import { code2hash, hash2code } from './helpers.mjs';
import { isTauri } from '../tauri.mjs';
const { latestCode } = settingsMap.get();
@ -43,14 +37,22 @@ const modules = [
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
import('@strudel.cycles/serial'),
import('@strudel.cycles/soundfonts'),
import('@strudel.cycles/csound'),
];
if (isTauri()) {
modules.concat([
import('@strudel/desktopbridge/loggerbridge.mjs'),
import('@strudel/desktopbridge/midibridge.mjs'),
import('@strudel/desktopbridge/oscbridge.mjs'),
]);
} else {
modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]);
}
const modulesLoading = evalScope(
controls, // sadly, this cannot be exported from core direclty
@ -73,11 +75,11 @@ async function initCode() {
try {
const initialUrl = window.location.href;
const hash = initialUrl.split('?')[1]?.split('#')?.[0];
const codeParam = window.location.href.split('#')[1];
const codeParam = window.location.href.split('#')[1] || '';
// looking like https://strudel.tidalcycles.org/?J01s5i1J0200 (fixed hash length)
if (codeParam) {
// looking like https://strudel.tidalcycles.org/#ImMzIGUzIg%3D%3D (hash length depends on code length)
return atob(decodeURIComponent(codeParam || ''));
return hash2code(codeParam);
} else if (hash) {
return supabase
.from('code')
@ -125,6 +127,8 @@ export function Repl({ embedded = false }) {
panelPosition,
} = useSettings();
const paintOptions = useMemo(() => ({ fontFamily }), [fontFamily]);
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
useStrudel({
initialCode: '// LOADING...',
@ -140,13 +144,15 @@ export function Repl({ embedded = false }) {
setMiniLocations(meta.miniLocations);
setPending(false);
setLatestCode(code);
window.location.hash = '#' + encodeURIComponent(btoa(code));
window.location.hash = '#' + code2hash(code);
},
onEvalError: (err) => {
setPending(false);
},
onToggle: (play) => !play && cleanupDraw(false),
drawContext,
// drawTime: [0, 6],
paintOptions,
});
// init code

View File

@ -0,0 +1,25 @@
export function unicodeToBase64(text) {
const utf8Bytes = new TextEncoder().encode(text);
const base64String = btoa(String.fromCharCode(...utf8Bytes));
return base64String;
}
export function base64ToUnicode(base64String) {
const utf8Bytes = new Uint8Array(
atob(base64String)
.split('')
.map((char) => char.charCodeAt(0)),
);
const decodedText = new TextDecoder().decode(utf8Bytes);
return decodedText;
}
export function code2hash(code) {
return encodeURIComponent(unicodeToBase64(code));
//return '#' + encodeURIComponent(btoa(code));
}
export function hash2code(hash) {
return base64ToUnicode(decodeURIComponent(hash));
//return atob(decodeURIComponent(codeParam || ''));
}

View File

@ -1,5 +1,5 @@
import { Pattern, noteToMidi, valueToMidi } from '@strudel.cycles/core';
import { registerSynthSounds, samples } from '@strudel.cycles/webaudio';
import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel.cycles/webaudio';
import './piano.mjs';
import './files.mjs';
@ -8,6 +8,7 @@ export async function prebake() {
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
await Promise.all([
registerSynthSounds(),
registerZZFXSounds(),
//registerSoundfonts(),
// need dynamic import here, because importing @strudel.cycles/soundfonts fails on server:
// => getting "window is not defined", as soon as "@strudel.cycles/soundfonts" is imported statically

View File

@ -34,6 +34,7 @@ import strudelTheme from '@strudel.cycles/react/src/themes/strudel-theme';
import bluescreen, { settings as bluescreenSettings } from '@strudel.cycles/react/src/themes/bluescreen';
import blackscreen, { settings as blackscreenSettings } from '@strudel.cycles/react/src/themes/blackscreen';
import whitescreen, { settings as whitescreenSettings } from '@strudel.cycles/react/src/themes/whitescreen';
import teletext, { settings as teletextSettings } from '@strudel.cycles/react/src/themes/teletext';
import algoboy, { settings as algoboySettings } from '@strudel.cycles/react/src/themes/algoboy';
import terminal, { settings as terminalSettings } from '@strudel.cycles/react/src/themes/terminal';
@ -42,6 +43,7 @@ export const themes = {
bluescreen,
blackscreen,
whitescreen,
teletext,
algoboy,
terminal,
abcdef,
@ -95,6 +97,7 @@ export const settings = {
bluescreen: bluescreenSettings,
blackscreen: blackscreenSettings,
whitescreen: whitescreenSettings,
teletext: teletextSettings,
algoboy: algoboySettings,
terminal: terminalSettings,
abcdef: {
@ -469,3 +472,11 @@ function getCircularReplacer() {
function stringifySafe(json) {
return JSON.stringify(json, getCircularReplacer());
}
export function injectStyle(rule) {
const newStyle = document.createElement('style');
document.head.appendChild(newStyle);
const styleSheet = newStyle.sheet;
const ruleIndex = styleSheet.insertRule(rule, 0);
return () => styleSheet.deleteRule(ruleIndex);
}

View File

@ -229,7 +229,9 @@ stack(
.add("0,.02")
.note().gain(.3)
.clip("<1@3 [.3 1]>/2")
.s('sawtooth').cutoff(600).color('#F8E71C'),
.cutoff(600)
.lpa(.2).lpenv(-4)
.s('sawtooth').color('#F8E71C'),
).fast(3/2)
//.pianoroll({fold:1})`;
@ -430,7 +432,7 @@ export const waa2 = `// "Waa2"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
n(
note(
"a4 [a3 c3] a3 c3"
.sub("<7 12 5 12>".slow(2))
.off(1/4,x=>x.add(7))
@ -442,7 +444,7 @@ n(
.cutoff(cosine.range(500,4000).slow(16))
.gain(.5)
.room(.5)
`;
.lpa(.125).lpenv(-2).v("8:.125").fanchor(.25)`;
export const hyperpop = `// "Hyperpop"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
@ -468,7 +470,9 @@ stack(
.note()
.s("sawtooth,square")
.gain(.3).attack(0.01).decay(0.1).sustain(.5)
.apply(filter1),
.apply(filter1)
.lpa(.1).lpenv(2).ftype('24db')
,
"~@3 [<2 3>,<4 5>]"
.echo(4,1/16,.7)
.scale(scales)
@ -537,6 +541,7 @@ stack(
.s('sawtooth') // waveform
.gain(.4) // turn down
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff
.lpa(.1).lpenv(-2)
//.hush()
,chord("<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>")
.dict('lefthand').voicing() // chords
@ -559,6 +564,7 @@ stack(
)
.slow(3/2)`;
/*
export const outroMusic = `// "Outro music"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
@ -576,8 +582,8 @@ chord("<C^7 Am7 Dm7 G7>*2").dict('lefthand').anchor("G4").voicing()
.s("gm_epiano1:1")
.color('steelblue')
.stack(
n("<-7 ~@2 [~@2 -7] -9 ~@2 [~@2 -9] -10!2 ~ [~@2 -10] -5 ~ [-3 -2 -10]@2>*2")
.scale('C3 major')
"<-7 ~@2 [~@2 -7] -9 ~@2 [~@2 -9] -10!2 ~ [~@2 -10] -5 ~ [-3 -2 -10]@2>*2"
.scale('C3 major').note()
.s('sawtooth').color('brown')
)
.attack(0.05).decay(.1).sustain(.7)
@ -589,7 +595,8 @@ chord("<C^7 Am7 Dm7 G7>*2").dict('lefthand').anchor("G4").voicing()
.n(3).color('gray')
).slow(3/2)
//.pianoroll({autorange:1,vertical:1,fold:0})
`;
`;
*/
export const bassFuge = `// "Bass fuge"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
@ -772,7 +779,8 @@ stack(
.gain("0.4,0.4(5,8,-1)"),
note("<0 2 5 3>".scale('G1 minor')).struct("x(5,8,-1)")
.s('sawtooth').decay(.1).sustain(0),
.s('sawtooth').decay(.1).sustain(0)
.lpa(.1).lpenv(-4).lpf(800).lpq(8),
note("<G4 A4 Bb4 A4>,Bb3,D3").struct("~ x*2").s('square').clip(1)
.cutoff(sine.range(500,4000).slow(16)).resonance(10)
@ -803,14 +811,16 @@ stack(
sine.add(saw.slow(4)).range(0,7).segment(8)
.superimpose(x=>x.add(.1))
.scale('G0 minor').note()
.s("sawtooth").decay(.1).sustain(0)
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
.s("sawtooth")
.gain(.4).decay(.1).sustain(0)
.lpa(.1).lpenv(-4).lpq(10)
.cutoff(perlin.range(300,3000).slow(8))
.degradeBy("0 0.1 .5 .1")
.rarely(add(note("12")))
,
// chord
note("Bb3,D4".superimpose(x=>x.add(.2)))
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
.s('sawtooth').lpf(1000).struct("<~@3 [~ x]>")
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
,
// alien
@ -827,8 +837,8 @@ note("c3 eb3 g3 bb3").palindrome()
.s('sawtooth')
.jux(x=>x.rev().color('green').s('sawtooth'))
.off(1/4, x=>x.add(note("<7 12>/2")).slow(2).late(.005).s('triangle'))
//.delay(.5)
.fast(1).cutoff(sine.range(200,2000).slow(8))
.lpf(sine.range(200,2000).slow(8))
.lpa(.2).lpenv(-2)
.decay(.05).sustain(0)
.room(.6)
.delay(.5).delaytime(.1).delayfeedback(.4)
@ -907,7 +917,13 @@ n("[0,3] 2 [1,3] 2".fast(3).lastOf(4, fast(2))).clip(2)
.delay(.2)
.room(.5).pan(sine.range(.3,.6))
.s('piano')
.stack("<<A1 C2>!2 F2 [F2 E2]>".add.out("0 -5".fast(2)).add("0,.12").note().s('sawtooth').clip(1).cutoff(300))
.stack(
"<<A1 C2>!2 F2 F2>"
.add.out("0 -5".fast(2))
.add("0,.12").note()
.s('sawtooth').cutoff(180)
.lpa(.1).lpenv(2)
)
.slow(4)
.stack(s("bd*4, [~ [hh hh? hh?]]*2,~ [sd ~ [sd:2? bd?]]").bank('RolandTR909').gain(.5).slow(2))
`;

View File

@ -40,13 +40,15 @@ export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
const patternSetting = (key) =>
register(key, (value, pat) => {
value = Array.isArray(value) ? value.join(' ') : value;
if (value !== settingsMap.get()[key]) {
settingsMap.setKey(key, value);
}
return pat;
});
register(key, (value, pat) =>
pat.onTrigger(() => {
value = Array.isArray(value) ? value.join(' ') : value;
if (value !== settingsMap.get()[key]) {
settingsMap.setKey(key, value);
}
return pat;
}, false),
);
export const theme = patternSetting('theme');
export const fontFamily = patternSetting('fontFamily');

View File

@ -26,6 +26,14 @@
font-family: 'FiraCode-SemiBold';
src: url('/fonts/FiraCode/FiraCode-SemiBold.ttf');
}
@font-face {
font-family: 'teletext';
src: url('/fonts/teletext/EuropeanTeletext.ttf');
}
@font-face {
font-family: 'mode7';
src: url('/fonts/mode7/MODE7GX3.TTF');
}
.prose > h1:not(:first-child) {
margin-top: 30px;

4
website/src/tauri.mjs Normal file
View File

@ -0,0 +1,4 @@
import { invoke } from '@tauri-apps/api/tauri';
export const Invoke = invoke;
export const isTauri = () => window.__TAURI_IPC__ != null;