diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 21a6d9624..c7edef74f 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -72,6 +72,27 @@ export function registerControl(names, ...aliases) { return bag; } +export function registerMultiControl(names, maxControls, ...aliases) { + names = Array.isArray(names) ? names : [names]; + let bag = {}; + for (let i = 1; i <= maxControls; i++) { + let theseAliases = [...aliases]; + let theseNames = [...names]; + if (i === 1) { + // adds e.g. fm1 as an alias for fm + const aliases1 = theseAliases.map((a) => `${a}1`); + const names1 = theseNames.map((n) => `${n}1`); + theseAliases = theseAliases.concat(aliases1).concat(names1); + } else { + theseAliases = theseAliases.map((a) => `${a}${i}`); + theseNames = theseNames.map((n) => `${n}${i}`); + } + const subBag = registerControl(theseNames, ...theseAliases); + bag = { ...bag, ...subBag }; + } + return bag; +} + /** * Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters * separated by ':'. @@ -380,12 +401,13 @@ export const { accelerate } = registerControl('accelerate'); * Sets the velocity from 0 to 1. Is multiplied together with gain. * * @name velocity + * @synonyms vel * @example * s("hh*8") * .gain(".4!2 1 .4!2 1 .4 1") * .velocity(".4 1") */ -export const { velocity } = registerControl('velocity'); +export const { velocity, vel } = registerControl('velocity', 'vel'); /** * Controls the gain by an exponential amount. * @@ -435,6 +457,9 @@ export const { attack, att } = registerControl('attack', 'att'); * Whole numbers and simple ratios sound more natural, * while decimal numbers and complex ratios sound metallic. * + * A number may be added afterwards to control the harmonicity of + * any of the 8 individual FMs (e.g. `fmh2`) + * * @name fmh * @param {number | Pattern} harmonicity * @example @@ -444,25 +469,40 @@ export const { attack, att } = registerControl('attack', 'att'); * ._scope() * */ -export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh'); +export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerMultiControl(['fmh', 'fmi'], 8, 'fmh'); + /** * Sets the Frequency Modulation of the synth. * Controls the modulation index, which defines the brightness of the sound. * - * @name fm + * A number may be added afterwards to control the modulation index of + * any of the 8 individual FMs (e.g. `fm3`). Also, FMs may be routed into + * each other with matrix commands like `fm13`, which would send `fm1` back into + * `fm3` + * + * @name fmi * @param {number | Pattern} brightness modulation index - * @synonyms fmi + * @synonyms fm * @example * note("c e g b g e") * .fm("<0 1 2 8 32>") * ._scope() + * @example + * s("sine").note("F1").seg(8) + * .fm(4).fm2(rand.mul(4)).fm3(saw.mul(8).slow(8)) + * .fmh(1.06).fmh2(10).fmh3(0.1) * */ -export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm'); +export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8 } = + registerMultiControl(['fmi', 'fmh'], 8, 'fm'); + // fm envelope /** * Ramp type of fm envelope. Exp might be a bit broken.. * + * A number may be added afterwards to control the envelope of + * any of the 8 individual FMs (e.g. `fmenv4`) + * * @name fmenv * @param {number | Pattern} type lin | exp * @example @@ -474,11 +514,19 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm'); * ._scope() * */ -export const { fmenv } = registerControl('fmenv'); +export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8 } = registerMultiControl( + 'fmenv', + 8, +); + /** * Attack time for the FM envelope: time it takes to reach maximum modulation * + * A number may be added afterwards to control the attack of the envelope of + * any of the 8 individual FMs (e.g. `fmatt5`) + * * @name fmattack + * @synonyms fmatt * @param {number | Pattern} time attack time * @example * note("c e g b g e") @@ -487,11 +535,33 @@ export const { fmenv } = registerControl('fmenv'); * ._scope() * */ -export const { fmattack } = registerControl('fmattack'); +export const { + fmattack, + fmattack1, + fmattack2, + fmattack3, + fmattack4, + fmattack5, + fmattack6, + fmattack7, + fmattack8, + fmatt, + fmatt1, + fmatt2, + fmatt3, + fmatt4, + fmatt5, + fmatt6, + fmatt7, + fmatt8, +} = registerMultiControl('fmattack', 8, 'fmatt'); /** * Waveform of the fm modulator * + * A number may be added afterwards to control the waveform + * any of the 8 individual FMs (e.g. `fmwave6`) + * * @name fmwave * @param {number | Pattern} wave waveform * @example @@ -500,12 +570,19 @@ export const { fmattack } = registerControl('fmattack'); * n("0 1 2 3".fast(4)).chord("").voicing().s("sawtooth").fmwave("brown").fm(.6) * */ -export const { fmwave } = registerControl('fmwave'); +export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmwave7, fmwave8 } = registerMultiControl( + 'fmwave', + 8, +); /** * Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase. * + * A number may be added afterwards to control the decay of the envelope of + * any of the 8 individual FMs (e.g. `fmdec6`) + * * @name fmdecay + * @synonyms fmdec * @param {number | Pattern} time decay time * @example * note("c e g b g e") @@ -515,11 +592,35 @@ export const { fmwave } = registerControl('fmwave'); * ._scope() * */ -export const { fmdecay } = registerControl('fmdecay'); +export const { + fmdecay, + fmdecay1, + fmdecay2, + fmdecay3, + fmdecay4, + fmdecay5, + fmdecay6, + fmdecay7, + fmdecay8, + fmdec, + fmdec1, + fmdec2, + fmdec3, + fmdec4, + fmdec5, + fmdec6, + fmdec7, + fmdec8, +} = registerMultiControl('fmdecay', 8, 'fmdec'); + /** * Sustain level for the FM envelope: how much modulation is applied after the decay phase * + * A number may be added afterwards to control the sustain of the envelope of + * any of the 8 individual FMs (e.g. `fmsus7`) + * * @name fmsustain + * @synonyms fmsus * @param {number | Pattern} level sustain level * @example * note("c e g b g e") @@ -529,10 +630,69 @@ export const { fmdecay } = registerControl('fmdecay'); * ._scope() * */ -export const { fmsustain } = registerControl('fmsustain'); -// these are not really useful... skipping for now -export const { fmrelease } = registerControl('fmrelease'); -export const { fmvelocity } = registerControl('fmvelocity'); +export const { + fmsustain, + fmsustain1, + fmsustain2, + fmsustain3, + fmsustain4, + fmsustain5, + fmsustain6, + fmsustain7, + fmsustain8, + fmsus, + fmsus1, + fmsus2, + fmsus3, + fmsus4, + fmsus5, + fmsus6, + fmsus7, + fmsus8, +} = registerMultiControl('fmsustain', 8, 'fmsus'); + +/** + * Release time for the FM envelope: how much modulation is applied after the note is released + * + * A number may be added afterwards to control the release of the envelope of + * any of the 8 individual FMs (e.g. `fmrel8`) + * + * @name fmrelease + * @synonyms fmrel + * @param {number | Pattern} time release time + * + */ +export const { + fmrelease, + fmrelease1, + fmrelease2, + fmrelease3, + fmrelease4, + fmrelease5, + fmrelease6, + fmrelease7, + fmrelease8, + fmrel, + fmrel1, + fmrel2, + fmrel3, + fmrel4, + fmrel5, + fmrel6, + fmrel7, + fmrel8, +} = registerMultiControl('fmrelease', 8, 'fmrel'); + +// FM Matrix +// Note: we do not declare top-level exports here since it would add +// ~162 more explicit exports. This is likely fine as the most common use-case would be to at least +// declare one other FM prior to utilizing the matrix functionality, but if we ever decide we need it, +// TODO to add it explicitly / go with the globalThis approach +for (let i = 0; i <= 8; i++) { + for (let j = 0; j <= 8; j++) { + registerControl(`fmi${i}${j}`, `fm${i}${j}`); + } +} /** * Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`. @@ -1323,14 +1483,13 @@ export const { lpdepth } = registerControl('lpdepth'); * Depth of the LFO for the lowpass filter, in HZ * * @name lpdepthfrequency - * @synonyms - * lpdethfreq + * @synonyms lpdepthfreq * @param {number | Pattern} depth depth of modulation * @example * note("*16").s("sawtooth").lpf(600).lpdepthfrequency("<200 500 100 0>") */ -export const { lpdepthfrequency } = registerControl('lpdepthfrequency', 'lpdepthfreq'); +export const { lpdepthfrequency, lpdepthfreq } = registerControl('lpdepthfrequency', 'lpdepthfreq'); /** * Shape of the LFO for the lowpass filter @@ -1384,14 +1543,13 @@ export const { bpdepth } = registerControl('bpdepth'); * Depth of the LFO for the bandpass filter, in HZ * * @name bpdepthfrequency - * @synonyms - * bpdethfreq + * @synonyms bpdepthfreq * @param {number | Pattern} depth depth of modulation * @example * note("*16").s("sawtooth").lpf(600).bpdepthfrequency("<200 500 100 0>") */ -export const { bpdepthfrequency } = registerControl('bpdepthfrequency', 'bpdepthfreq'); +export const { bpdepthfrequency, bpdepthfreq } = registerControl('bpdepthfrequency', 'bpdepthfreq'); /** * Shape of the LFO for the bandpass filter @@ -1439,20 +1597,19 @@ export const { hpsync } = registerControl('hpsync'); * @name hpdepth * @param {number | Pattern} depth depth of modulation */ -export const { hpdepth, hpdepthfreq } = registerControl('hpdepth'); +export const { hpdepth } = registerControl('hpdepth'); /** * Depth of the LFO for the hipass filter, in hz * * @name hpdepthfrequency - * @synonyms - * hpdethfreq + * @synonyms hpdepthfreq * @param {number | Pattern} depth depth of modulation * @example * note("*16").s("sawtooth").lpf(600).hpdepthfrequency("<200 500 100 0>") */ -export const { hpdepthfrequency } = registerControl('hpdepthfrequency', 'hpdepthfreq'); +export const { hpdepthfrequency, hpdepthfreq } = registerControl('hpdepthfrequency', 'hpdepthfreq'); /** * Shape of the LFO for the highpass filter @@ -2629,3 +2786,17 @@ export const scrub = register( }, false, ); + +/** + * Transient shaper. Gives independent control over the emphasis on transients + * and sustains + * + * @name transient + * @param {number | Pattern} attack Emphasis on transients; between -1 (deaccentuate) and 1 (accentuate) + * @param {number | Pattern} sustain Emphasis on the sustains; between -1 (deaccentuate) and 1 (accentuate) + * @example + * s("bd").transient("<-1 -0.5 0 0.5 1>") + * @example + * s("hh*16").bank("tr909").transient("<-1:1 1:-1>") + */ +export const { transient } = registerControl(['transient', 'transsustain']); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index c2e5ca1bc..6f214386f 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -574,7 +574,8 @@ export class Pattern { * Returns a new Pattern, which only returns haps that meet the given test. * @param {Function} hap_test - a function which returns false for haps to be removed from the pattern * @returns Pattern - * @noAutocomplete + * @example + * s("bd*8").velocity(rand).filterHaps((h) => (h.whole.begin % 1) < h.value.velocity) */ filterHaps(hap_test) { return new Pattern((state) => this.query(state).filter(hap_test)); @@ -585,7 +586,11 @@ export class Pattern { * inside haps. * @param {Function} value_test * @returns Pattern - * @noAutocomplete + * @example + * const drums = s("bd sd bd sd") + * kick: drums.filterValues((v) => v.s === 'bd').duck(2) + * snare: drums.filterValues((v) => v.s === 'sd') + * bass: s("saw!4").note("G#1").lpf(80).lpenv(4).orbit(2) */ filterValues(value_test) { return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setSteps(this._steps); @@ -1587,7 +1592,13 @@ export const func = curry((a, b) => reify(b).func(a)); * * @param {string | string[]} name name of the function, or an array of names to be used as synonyms * @param {function} func function with 1 or more params, where last is the current pattern - * @noAutocomplete + * @param {bool} patternify defaults to true; if set to false, you will have more control over the arguments to `func` as they will be + * in their raw form and it will be up to you to patternify them and/or query them for values + * @example + * const vlpf = register('vlpf', (freq, pat) => { + * return pat.fmap((v) => ({...v, cutoff: freq * (v.velocity ?? 1) })); + * }) + * s("saw").seg(8).velocity(rand).vlpf(800) * */ export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) { @@ -2676,8 +2687,13 @@ export const hsl = register('hsl', (h, s, l, pat) => { /** * Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array). * @name tag - * @noAutocomplete * @param {string} tag anything unique + * @example + * s("saw!16").note("F1") + * .lpf(tri.range(40, 80).slow(4)).lpenv(5).lpq(4).lpd(0.15) + * .when(rand.late(0.1).gte(0.5), x => x.transpose("12").tag('altered')) + * .when(rand.late(0.2).gte(0.5), x => x.s("square").tag('altered')) + * .when("<0 1>", x => x.filter((hap) => hap.hasTag('altered'))) */ Pattern.prototype.tag = function (tag) { return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) })); @@ -2688,15 +2704,16 @@ Pattern.prototype.tag = function (tag) { * @name filter * @param {Function} test function to test Hap * @example - * s("hh!7 oh").filter(hap => hap.value.s==='hh') + * s("hh!7 oh").filter(hap => hap.value.s === 'hh') */ export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test))); /** * Filters haps by their begin time * @name filterWhen - * @noAutocomplete * @param {Function} test function to test Hap.whole.begin + * @example + * oneCycle: s("bd*4").filterWhen((t) => t < 1) */ export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin))); diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 32f030125..cd38d3587 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -479,14 +479,24 @@ Pattern.prototype.midi = function (midiport, options = {}) { let listeners = {}; const refs = {}; +const refsByChan = {}; /** * MIDI input: Opens a MIDI input port to receive MIDI control change messages. + * + * The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel * @param {string | number} input MIDI device name or index defaulting to 0 - * @returns {Function} + * @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern. + * When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1) + * that came through that cc number (and channel, if provided) * @example - * let cc = await midin('IAC Driver Bus 1') + * const cc = await midin('IAC Driver Bus 1') * note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth") + * @example + * const allCC = await midin('IAC Driver Bus 1') + * const cc = (ccNum) => allCC(ccNum, 2) // just channel 2 + * note("c a f e").s("saw") + * .when(cc(0).gt(0), x => x.postgain(0)) */ export async function midin(input) { if (isPattern(input)) { @@ -511,17 +521,24 @@ export async function midin(input) { }`, ); } - // ensure refs for this input are initialized - if (!refs[input]) { - refs[input] = {}; - } - const cc = (cc) => ref(() => refs[input][cc] || 0); + refs[input] ??= {}; + refsByChan[input] ??= {}; + const cc = (cc, chan) => { + if (chan !== undefined) { + return ref(() => refsByChan[input][cc]?.[chan] || 0); + } + return ref(() => refs[input][cc] || 0); + }; listeners[input] && device.removeListener('midimessage', listeners[input]); listeners[input] = (e) => { - const cc = e.dataBytes[0]; + const ccNum = e.dataBytes[0]; const v = e.dataBytes[1]; - refs[input] && (refs[input][cc] = v / 127); + const chan = e.message.channel; + const scaled = v / 127; + refsByChan[input][ccNum] ??= {}; + refsByChan[input][ccNum][chan] = scaled; + refs[input][ccNum] = scaled; }; device.addListener('midimessage', listeners[input]); return cc; diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 427f57e31..ca9e45d51 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -6,6 +6,7 @@ import { getADSRValues, getPitchEnvelope, getVibratoOscillator, + onceEnded, } from '@strudel/webaudio'; import gm from './gm.mjs'; @@ -170,12 +171,12 @@ export function registerSoundfonts() { bufferSource.stop(envEnd); const stop = (releaseTime) => {}; - bufferSource.onended = () => { + onceEnded(bufferSource, () => { bufferSource.disconnect(); vibratoOscillator?.stop(); node.disconnect(); onended(); - }; + }); return { node, stop }; }, { type: 'soundfont', prebake: true, fonts }, diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index ed2c0c448..0b696df9b 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,7 +1,7 @@ import { getAudioContext } from './audioContext.mjs'; -import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs'; -import { getNoiseBuffer } from './noise.mjs'; import { logger } from './logger.mjs'; +import { getNoiseBuffer } from './noise.mjs'; +import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs'; export const noises = ['pink', 'white', 'brown', 'crackle']; @@ -349,25 +349,17 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { constantNode.connect(zeroGain); // Schedule the `onComplete` callback to occur at `stopTime` - constantNode.onended = () => { - // Ensure garbage collection - try { - zeroGain.disconnect(); - } catch { - // pass - } - try { - constantNode.disconnect(); - } catch { - // pass - } + onceEnded(constantNode, () => { + releaseAudioNode(zeroGain); + releaseAudioNode(constantNode); onComplete(); - }; + }); constantNode.start(startTime); constantNode.stop(stopTime); return constantNode; } -const mod = (freq, range = 1, type = 'sine') => { + +const mod = (freq, type = 'sine') => { const ctx = getAudioContext(); let osc; if (noises.includes(type)) { @@ -379,69 +371,89 @@ const mod = (freq, range = 1, type = 'sine') => { osc.type = type; osc.frequency.value = freq; } - osc.start(); - const g = gainNode(range); - osc.connect(g); // -range, range - return { node: g, stop: (t) => osc.stop(t), osc: osc }; + return osc; }; -const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => { + +const fm = (frequencyparam, harmonicityRatio, wave = 'sine') => { const carrfreq = frequencyparam.value; const modfreq = carrfreq * harmonicityRatio; - const modgain = modfreq * modulationIndex; - return mod(modfreq, modgain, wave); + return { osc: mod(modfreq, wave), freq: modfreq }; }; + export function applyFM(param, value, begin) { - const { - fmh: fmHarmonicity = 1, - fmi: fmModulationIndex, - fmenv: fmEnvelopeType = 'exp', - fmattack: fmAttack, - fmdecay: fmDecay, - fmsustain: fmSustain, - fmrelease: fmRelease, - fmvelocity: fmVelocity, - fmwave: fmWaveform = 'sine', - duration, - } = value; - let modulator; - let stop = () => {}; - - if (fmModulationIndex) { - const ac = getAudioContext(); - const envGain = ac.createGain(); - const fmmod = fm(param, fmHarmonicity, fmModulationIndex, fmWaveform); - - modulator = fmmod.node; - stop = fmmod.stop; - if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) { - // no envelope by default - modulator.connect(param); - } else { - const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]); - const holdEnd = begin + duration; - getParamADSR( - envGain.gain, - attack, - decay, - sustain, - release, - 0, - 1, - begin, - holdEnd, - fmEnvelopeType === 'exp' ? 'exponential' : 'linear', - ); - modulator.connect(envGain); - envGain.connect(param); + const ac = getAudioContext(); + const toStop = []; // fm oscillators we will expose `stop` for + const fms = {}; + // Matrix + for (let i = 1; i <= 8; i++) { + for (let j = 0; j <= 8; j++) { + let control; + if (i === j + 1) { + // Standard fm3 -> fm2 -> fm1 -> param usage + const iS = i === 1 ? '' : i; + control = `fmi${iS}`; + } else { + control = `fmi${i}${j}`; + } + const amt = value[control]; + if (!amt) continue; + let io = []; + for (let [isMod, idx] of [ + [true, i], // source + [false, j], // target + ]) { + if (idx === 0) { + io.push(param); + continue; + } + if (!fms[idx]) { + const idxS = idx === 1 ? '' : idx; + const { osc, freq } = fm(param, value[`fmh${idxS}`] ?? 1, value[`fmwave${idxS}`] ?? 'sine'); + toStop.push(osc); + const toCleanup = [osc]; // nodes we want to cleanup after oscillator `stop` + const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${idxS}`]); + let output = osc; + if (adsr.some((v) => v !== undefined)) { + const envGain = ac.createGain(); + const [attack, decay, sustain, release] = getADSRValues(adsr); + const holdEnd = begin + value.duration; + const fmEnvelopeType = value[`fmenv${idxS}`] ?? 'exp'; + getParamADSR( + envGain.gain, + attack, + decay, + sustain, + release, + 0, + 1, + begin, + holdEnd, + fmEnvelopeType === 'exp' ? 'exponential' : 'linear', + ); + toCleanup.push(envGain); + output = osc.connect(envGain); + } + fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup }; + } + const { input, output, freq, osc, toCleanup } = fms[idx]; + const g = gainNode(amt * freq); + io.push(isMod ? output.connect(g) : input); + cleanupOnEnd(osc, [...toCleanup, g]); + } + if (!io[1]) { + logger( + `[superdough] control ${control} failed to connect FM ${i} to target ${j} due to missing frequency parameter (likely because fm${j} is noise)`, + 'warning', + ); + continue; + } + io[0].connect(io[1]); } - fmmod.osc.onended = () => { - envGain.disconnect(); - modulator.disconnect(); - fmmod.osc.disconnect(); - }; } - return { stop }; + return { + stop: (t) => toStop.forEach((m) => m?.stop(t)), + }; } // Saturation curves @@ -554,10 +566,58 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => { return Number(freq); }; -export const destroyAudioWorkletNode = (node) => { - if (node == null) { - return; - } - node.disconnect(); - node.parameters.get('end')?.setValueAtTime(0, 0); +// This helper should be used instead of the `node.onended = callback` pattern +// It adds a mechanism to help minimize gc retention +export const onceEnded = (node, callback) => { + const onended = callback; + node.onended = function cleanup() { + onended && onended(); + this.onended = null; + }; +}; + +export const releaseAudioNode = (node) => { + if (node == null) return; + + // check we received an AudioNode + if (!(node instanceof AudioNode)) { + throw new Error('releaseAudioNode can only release an AudioNode'); + } + + // https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect + node.disconnect(); + + // make sure all AudioScheduledSourceNodes are in a stopped state + // https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode + if (node instanceof AudioScheduledSourceNode) { + if (node.onended && node.onended.name !== 'cleanup') { + logger( + `[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`, + ); + } + try { + node.stop(); + } catch (e) { + // At the stage, `start` was not called on the node + // but an `onended` callback releasing resources may exist + // and we want it to fire : + // - we force a start/stop cycle so that `onended` gets called + // - we `lock` the node so that no-one can start it + node.start(node.context.currentTime + 5); // will never happen + node.stop(); + } + } + + // https://www.w3.org/TR/webaudio-1.1/#AudioNode-actively-processing + // An AudioWorkletNode is actively processing when its AudioWorkletProcessor's [[callable process]] + // returns true and either its active source flag is true or + // any AudioNode connected to one of its inputs is actively processing. + if (node instanceof AudioWorkletNode) { + node.parameters.get('end')?.setValueAtTime(0, 0); + } +}; + +// Once the `anchor` node has ended, release all nodes in `toCleanup` +export const cleanupOnEnd = (anchor, toCleanup) => { + onceEnded(anchor, () => toCleanup.forEach((n) => releaseAudioNode(n))); }; diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index aaa1a8e48..bdf524d10 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,7 +1,14 @@ import { getBaseURL, getCommonSampleInfo } from './util.mjs'; import { registerSound, registerWaveTable } from './index.mjs'; import { getAudioContext } from './audioContext.mjs'; -import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; +import { + getADSRValues, + getParamADSR, + getPitchEnvelope, + getVibratoOscillator, + onceEnded, + releaseAudioNode, +} from './helpers.mjs'; import { logger } from './logger.mjs'; const bufferCache = {}; // string: Promise @@ -236,12 +243,6 @@ export async function fetchSampleMap(url) { * sd: '808sd/SD0010.WAV' * }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/'); * s("[bd ~]*2, [~ hh]*2, ~ sd") - * @example - * samples('shabda:noise,chimp:2') - * s("noise ") - * @example - * samples('shabda/speech/fr-FR/f:chocolat') - * s("chocolat*4") */ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => { @@ -286,17 +287,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl); - // asny stuff above took too long? - if (ac.currentTime > t) { - logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight'); - // console.warn('sample still loading:', s, n); - return; - } if (!bufferSource) { logger(`[sampler] could not load "${s}:${n}"`, 'error'); return; } + // async stuff above took too long? + if (ac.currentTime > t) { + logger(`[sampler] loading sound "${s}:${n}" took too long`, 'highlight'); + // AudioBufferSourceNode will never be used. discard it + releaseAudioNode(bufferSource); + return; + } + // vibrato let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); @@ -319,13 +322,13 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... node.connect(out); - bufferSource.onended = function () { + onceEnded(bufferSource, function () { bufferSource.disconnect(); vibratoOscillator?.stop(); node.disconnect(); out.disconnect(); onended(); - }; + }); let envEnd = holdEnd + release + 0.01; bufferSource.stop(envEnd); const stop = (endTime) => { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 03d56bc2a..b97865918 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -274,7 +274,7 @@ export async function initAudioOnFirstClick(options) { } let controller; -function getSuperdoughAudioController() { +export function getSuperdoughAudioController() { if (controller == null) { controller = new SuperdoughAudioController(getAudioContext()); } @@ -290,8 +290,8 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000 const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); //filters - const numStages = 2; //num of filters in series - let fOffset = 0; + const numStages = 1; //num of filters in series + let fOffset = 282; //for backward compat in #1800 const filterChain = []; for (let i = 0; i < numStages; i++) { const filter = ac.createBiquadFilter(); @@ -302,12 +302,9 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000 lfo.connect(filter.detune); fOffset += 282; - if (i > 0) { - filterChain[i - 1].connect(filter); - } filterChain.push(filter); } - return { phaser: filterChain[filterChain.length - 1], lfo }; + return { filterChain, lfo }; } function getFilterType(ftype) { @@ -451,6 +448,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) compressorKnee, compressorAttack, compressorRelease, + transient, + transsustain, } = value; delaytime = delaytime ?? cycleToSeconds(delaysync, cps); @@ -532,6 +531,23 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.push(sourceNode); stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); + transient !== undefined && + chain.push( + getWorklet( + ac, + 'transient-processor', + {}, + { + processorOptions: { + attack: transient, + sustain: transsustain, + begin: t, + end: endWithRelease, + }, + }, + ), + ); + // gain stage chain.push(gainNode(gain)); @@ -697,9 +713,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } // phaser if (phaserrate !== undefined && phaserdepth > 0) { - const { phaser, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep); + const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep); audioNodes.push(lfo); - chain.push(phaser); + chain.push(...filterChain); } // last gain diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 3b27bf6c1..b02354465 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -3,7 +3,6 @@ import { registerSound, soundMap } from './superdough.mjs'; import { getAudioContext } from './audioContext.mjs'; import { applyFM, - destroyAudioWorkletNode, gainNode, getADSRValues, getFrequencyFromValue, @@ -13,6 +12,7 @@ import { getVibratoOscillator, getWorklet, noises, + releaseAudioNode, webAudioTimeout, } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -192,8 +192,7 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(o); - envGain.disconnect(); + releaseAudioNode(o); onended(); fm?.stop(); vibratoOscillator?.stop(); @@ -270,8 +269,7 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(o); - envGain.disconnect(); + releaseAudioNode(o); onended(); }, begin, @@ -344,9 +342,8 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(o); - destroyAudioWorkletNode(lfo); - envGain.disconnect(); + releaseAudioNode(o); + releaseAudioNode(lfo); onended(); fm?.stop(); vibratoOscillator?.stop(); diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 3659edcd0..e9d810414 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -3,13 +3,13 @@ import { getBaseURL, getCommonSampleInfo } from './util.mjs'; import { applyFM, applyParameterModulators, - destroyAudioWorkletNode, getADSRValues, getFrequencyFromValue, getParamADSR, getPitchEnvelope, getVibratoOscillator, getWorklet, + releaseAudioNode, webAudioTimeout, } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -319,10 +319,9 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(source); + releaseAudioNode(source); vibratoOscillator?.stop(); fm?.stop(); - node.disconnect(); wtPosModulators?.disconnect(); wtWarpModulators?.disconnect(); onended(); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index bf633a63b..a59aeb756 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -11,6 +11,9 @@ const PI = Math.PI; const TWO_PI = 2 * PI; const INVSR = 1 / sampleRate; +const timeToCoeff = (t) => 1 - Math.exp(-INVSR / t); +const dbToLin = (db) => Math.pow(10, db / 20); + const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const mod = (n, m) => ((n % m) + m) % m; const lerp = (a, b, n) => n * (b - a) + a; @@ -508,13 +511,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { ]; } process(_input, outputs, params) { - if (currentTime <= params.begin[0]) { - return true; - } if (currentTime >= params.end[0]) { - // this.port.postMessage({ type: 'onended' }); + // should terminate return false; } + if (currentTime <= params.begin[0]) { + // keep alive + return true; + } const output = outputs[0]; const voices = params.voices[0]; // k-rate for (let i = 0; i < output[0].length; i++) { @@ -1383,3 +1387,82 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor); + +class TransientProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return []; + } + + constructor(options) { + super(); + this.gainCoeff = timeToCoeff(0.2); + this.avgGain = 1; + let { + attackTime = 0.003, + sustainTime = 0.08, + attack = 0, + sustain = 0, + sensitivity = 0.1, + mix = 1, + begin = 0, + end = 0, + } = options.processorOptions; + attackTime = clamp(attackTime, 0.0005, 0.05); + sustainTime = clamp(sustainTime, 0.01, 0.5); + this.attackCoeff = timeToCoeff(attackTime); + this.sustainCoeff = timeToCoeff(sustainTime); + this.attackAmt = clamp(attack, -1, 1); + this.sustainAmt = clamp(sustain, -1, 1); + this.scaling = 0.5 + 5 * clamp(sensitivity, 0, 1); + this.mix = clamp(mix, 0, 1); + this.begin = begin; + this.end = end; + this.attackEnv = new Float32Array(2); // assume stereo + this.sustainEnv = new Float32Array(2); + } + + process(inputs, outputs, _params) { + const input = inputs[0]; + const output = outputs[0]; + if (currentTime >= this.end) { + return false; + } + if (currentTime <= this.begin) { + return true; + } + const channels = input.length; + if (channels > this.attackEnv.length) { + this.attackEnv = new Float32Array(channels); + this.sustainEnv = new Float32Array(channels); + } + let avgGain = this.avgGain; + for (let ch = 0; ch < channels; ch++) { + let attEnv = this.attackEnv[ch]; + let susEnv = this.sustainEnv[ch]; + for (let n = 0; n < blockSize; n++) { + const sample = input[ch][n]; + const x = Math.abs(sample); + attEnv = lerp(attEnv, x, this.attackCoeff); + susEnv = lerp(susEnv, x, this.sustainCoeff); + const peakiness = clamp((this.scaling * (attEnv - susEnv)) / (susEnv + 1e-6), -1.5, 1.5); + const attScale = peakiness > 0 ? peakiness : 0; + const susScale = peakiness < 0 ? -peakiness : 0; + const attackGain = dbToLin(this.attackAmt * attScale * 18); + const sustainGain = dbToLin(this.sustainAmt * susScale * 36); + const gain = clamp(attackGain * sustainGain, 0, 8); + avgGain = lerp(avgGain, gain, this.gainCoeff); + const makeup = avgGain > 1e-3 ? 1 / avgGain : 1; + const wet = sample * gain * makeup; + let y = lerp(sample, wet, this.mix); + y /= 1 + Math.abs(y); // soft clip + output[ch][n] = y; + } + this.attackEnv[ch] = attEnv; + this.sustainEnv[ch] = susEnv; + } + this.avgGain = avgGain; + return true; + } +} + +registerProcessor('transient-processor', TransientProcessor); diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index 32db0395a..4a4c58c9c 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -3,6 +3,7 @@ import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound } from './superdough.mjs'; import { getAudioContext } from './audioContext.mjs'; import { buildSamples } from './zzfx_fork.mjs'; +import { onceEnded, releaseAudioNode } from './helpers.mjs'; export const getZZFX = (value, t) => { let { @@ -83,10 +84,10 @@ export function registerZZFXSounds() { wave, (t, value, onended) => { const { node: o } = getZZFX({ s: wave, ...value }, t); - o.onended = () => { - o.disconnect(); + onceEnded(o, () => { + releaseAudioNode(o); onended(); - }; + }); return { node: o, stop: () => {}, diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e4b46b97f..09d285b5d 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -4043,6 +4043,70 @@ exports[`runs examples > example "filter" example index 0 1`] = ` ] `; +exports[`runs examples > example "filterHaps" example index 0 1`] = ` +[ + "[ 1/8 → 1/4 | s:bd velocity:0.6852155700325966 ]", + "[ 1/4 → 3/8 | s:bd velocity:0.36975969187915325 ]", + "[ 3/8 → 1/2 | s:bd velocity:0.40139251574873924 ]", + "[ 1/1 → 9/8 | s:bd velocity:0.5195421651005745 ]", + "[ 9/8 → 5/4 | s:bd velocity:0.6724895145744085 ]", + "[ 5/4 → 11/8 | s:bd velocity:0.7287282031029463 ]", + "[ 3/2 → 13/8 | s:bd velocity:0.6084080748260021 ]", + "[ 2/1 → 17/8 | s:bd velocity:0.9595271199941635 ]", + "[ 17/8 → 9/4 | s:bd velocity:0.9033902939409018 ]", + "[ 9/4 → 19/8 | s:bd velocity:0.34548251144587994 ]", + "[ 19/8 → 5/2 | s:bd velocity:0.8365066405385733 ]", + "[ 3/1 → 25/8 | s:bd velocity:0.21728911064565182 ]", + "[ 25/8 → 13/4 | s:bd velocity:0.34324387833476067 ]", + "[ 13/4 → 27/8 | s:bd velocity:0.9923650715500116 ]", + "[ 27/8 → 7/2 | s:bd velocity:0.40869180113077164 ]", + "[ 29/8 → 15/4 | s:bd velocity:0.9391485787928104 ]", + "[ 15/4 → 31/8 | s:bd velocity:0.8121673222631216 ]", +] +`; + +exports[`runs examples > example "filterValues" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 1/4 → 1/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 1/2 → 3/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 3/4 → 1/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 1/1 → 5/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 5/4 → 3/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 3/2 → 7/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 7/4 → 2/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 2/1 → 9/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 9/4 → 5/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 5/2 → 11/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 11/4 → 3/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 3/1 → 13/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 13/4 → 7/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 7/2 → 15/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", + "[ 15/4 → 4/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]", +] +`; + +exports[`runs examples > example "filterWhen" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd ]", + "[ 1/4 → 1/2 | s:bd ]", + "[ 1/2 → 3/4 | s:bd ]", + "[ 3/4 → 1/1 | s:bd ]", + "[ 1/1 → 5/4 | s:bd ]", + "[ 5/4 → 3/2 | s:bd ]", + "[ 3/2 → 7/4 | s:bd ]", + "[ 7/4 → 2/1 | s:bd ]", + "[ 2/1 → 9/4 | s:bd ]", + "[ 9/4 → 5/2 | s:bd ]", + "[ 5/2 → 11/4 | s:bd ]", + "[ 11/4 → 3/1 | s:bd ]", + "[ 3/1 → 13/4 | s:bd ]", + "[ 13/4 → 7/2 | s:bd ]", + "[ 7/2 → 15/4 | s:bd ]", + "[ 15/4 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "firstOf" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:g3 ]", @@ -4092,35 +4156,6 @@ exports[`runs examples > example "floor" example index 0 1`] = ` ] `; -exports[`runs examples > example "fm" example index 0 1`] = ` -[ - "[ 0/1 → 1/6 | note:c fmi:0 ]", - "[ 1/6 → 1/3 | note:e fmi:0 ]", - "[ 1/3 → 1/2 | note:g fmi:0 ]", - "[ 1/2 → 2/3 | note:b fmi:0 ]", - "[ 2/3 → 5/6 | note:g fmi:0 ]", - "[ 5/6 → 1/1 | note:e fmi:0 ]", - "[ 1/1 → 7/6 | note:c fmi:1 ]", - "[ 7/6 → 4/3 | note:e fmi:1 ]", - "[ 4/3 → 3/2 | note:g fmi:1 ]", - "[ 3/2 → 5/3 | note:b fmi:1 ]", - "[ 5/3 → 11/6 | note:g fmi:1 ]", - "[ 11/6 → 2/1 | note:e fmi:1 ]", - "[ 2/1 → 13/6 | note:c fmi:2 ]", - "[ 13/6 → 7/3 | note:e fmi:2 ]", - "[ 7/3 → 5/2 | note:g fmi:2 ]", - "[ 5/2 → 8/3 | note:b fmi:2 ]", - "[ 8/3 → 17/6 | note:g fmi:2 ]", - "[ 17/6 → 3/1 | note:e fmi:2 ]", - "[ 3/1 → 19/6 | note:c fmi:8 ]", - "[ 19/6 → 10/3 | note:e fmi:8 ]", - "[ 10/3 → 7/2 | note:g fmi:8 ]", - "[ 7/2 → 11/3 | note:b fmi:8 ]", - "[ 11/3 → 23/6 | note:g fmi:8 ]", - "[ 23/6 → 4/1 | note:e fmi:8 ]", -] -`; - exports[`runs examples > example "fmattack" example index 0 1`] = ` [ "[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 ]", @@ -4237,6 +4272,72 @@ exports[`runs examples > example "fmh" example index 0 1`] = ` ] `; +exports[`runs examples > example "fmi" example index 0 1`] = ` +[ + "[ 0/1 → 1/6 | note:c fmi:0 ]", + "[ 1/6 → 1/3 | note:e fmi:0 ]", + "[ 1/3 → 1/2 | note:g fmi:0 ]", + "[ 1/2 → 2/3 | note:b fmi:0 ]", + "[ 2/3 → 5/6 | note:g fmi:0 ]", + "[ 5/6 → 1/1 | note:e fmi:0 ]", + "[ 1/1 → 7/6 | note:c fmi:1 ]", + "[ 7/6 → 4/3 | note:e fmi:1 ]", + "[ 4/3 → 3/2 | note:g fmi:1 ]", + "[ 3/2 → 5/3 | note:b fmi:1 ]", + "[ 5/3 → 11/6 | note:g fmi:1 ]", + "[ 11/6 → 2/1 | note:e fmi:1 ]", + "[ 2/1 → 13/6 | note:c fmi:2 ]", + "[ 13/6 → 7/3 | note:e fmi:2 ]", + "[ 7/3 → 5/2 | note:g fmi:2 ]", + "[ 5/2 → 8/3 | note:b fmi:2 ]", + "[ 8/3 → 17/6 | note:g fmi:2 ]", + "[ 17/6 → 3/1 | note:e fmi:2 ]", + "[ 3/1 → 19/6 | note:c fmi:8 ]", + "[ 19/6 → 10/3 | note:e fmi:8 ]", + "[ 10/3 → 7/2 | note:g fmi:8 ]", + "[ 7/2 → 11/3 | note:b fmi:8 ]", + "[ 11/3 → 23/6 | note:g fmi:8 ]", + "[ 23/6 → 4/1 | note:e fmi:8 ]", +] +`; + +exports[`runs examples > example "fmi" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | s:sine note:F1 fmi:4 fmi2:0 fmi3:0 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/8 → 1/4 | s:sine note:F1 fmi:4 fmi2:2.7408622801303864 fmi3:0.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/4 → 3/8 | s:sine note:F1 fmi:4 fmi2:1.479038767516613 fmi3:0.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/8 → 1/2 | s:sine note:F1 fmi:4 fmi2:1.605570062994957 fmi3:0.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/2 → 5/8 | s:sine note:F1 fmi:4 fmi2:1.041922464966774 fmi3:0.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 5/8 → 3/4 | s:sine note:F1 fmi:4 fmi2:0.5425434708595276 fmi3:0.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/4 → 7/8 | s:sine note:F1 fmi:4 fmi2:0.7833059206604958 fmi3:0.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 7/8 → 1/1 | s:sine note:F1 fmi:4 fmi2:1.590524323284626 fmi3:0.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/1 → 9/8 | s:sine note:F1 fmi:4 fmi2:2.078168660402298 fmi3:1 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 9/8 → 5/4 | s:sine note:F1 fmi:4 fmi2:2.689958058297634 fmi3:1.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 5/4 → 11/8 | s:sine note:F1 fmi:4 fmi2:2.914912812411785 fmi3:1.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 11/8 → 3/2 | s:sine note:F1 fmi:4 fmi2:0.7312150076031685 fmi3:1.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/2 → 13/8 | s:sine note:F1 fmi:4 fmi2:2.4336322993040085 fmi3:1.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 13/8 → 7/4 | s:sine note:F1 fmi:4 fmi2:1.987018845975399 fmi3:1.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 7/4 → 15/8 | s:sine note:F1 fmi:4 fmi2:0.8189515843987465 fmi3:1.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 15/8 → 2/1 | s:sine note:F1 fmi:4 fmi2:2.3782287165522575 fmi3:1.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 2/1 → 17/8 | s:sine note:F1 fmi:4 fmi2:3.838108479976654 fmi3:2 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 17/8 → 9/4 | s:sine note:F1 fmi:4 fmi2:3.613561175763607 fmi3:2.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 9/4 → 19/8 | s:sine note:F1 fmi:4 fmi2:1.3819300457835197 fmi3:2.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 19/8 → 5/2 | s:sine note:F1 fmi:4 fmi2:3.346026562154293 fmi3:2.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 5/2 → 21/8 | s:sine note:F1 fmi:4 fmi2:1.8344642966985703 fmi3:2.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 21/8 → 11/4 | s:sine note:F1 fmi:4 fmi2:0.6407739371061325 fmi3:2.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 11/4 → 23/8 | s:sine note:F1 fmi:4 fmi2:2.5329310819506645 fmi3:2.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 23/8 → 3/1 | s:sine note:F1 fmi:4 fmi2:0.06501668691635132 fmi3:2.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/1 → 25/8 | s:sine note:F1 fmi:4 fmi2:0.8691564425826073 fmi3:3 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 25/8 → 13/4 | s:sine note:F1 fmi:4 fmi2:1.3729755133390427 fmi3:3.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 13/4 → 27/8 | s:sine note:F1 fmi:4 fmi2:3.9694602862000465 fmi3:3.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 27/8 → 7/2 | s:sine note:F1 fmi:4 fmi2:1.6347672045230865 fmi3:3.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 7/2 → 29/8 | s:sine note:F1 fmi:4 fmi2:1.639795258641243 fmi3:3.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 29/8 → 15/4 | s:sine note:F1 fmi:4 fmi2:3.7565943151712418 fmi3:3.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 15/4 → 31/8 | s:sine note:F1 fmi:4 fmi2:3.2486692890524864 fmi3:3.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 31/8 → 4/1 | s:sine note:F1 fmi:4 fmi2:2.9446661546826363 fmi3:3.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", +] +`; + exports[`runs examples > example "fmsustain" example index 0 1`] = ` [ "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]", @@ -6947,6 +7048,27 @@ exports[`runs examples > example "midin" example index 0 1`] = ` ] `; +exports[`runs examples > example "midin" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | note:c s:saw ]", + "[ 1/4 → 1/2 | note:a s:saw ]", + "[ 1/2 → 3/4 | note:f s:saw ]", + "[ 3/4 → 1/1 | note:e s:saw ]", + "[ 1/1 → 5/4 | note:c s:saw ]", + "[ 5/4 → 3/2 | note:a s:saw ]", + "[ 3/2 → 7/4 | note:f s:saw ]", + "[ 7/4 → 2/1 | note:e s:saw ]", + "[ 2/1 → 9/4 | note:c s:saw ]", + "[ 9/4 → 5/2 | note:a s:saw ]", + "[ 5/2 → 11/4 | note:f s:saw ]", + "[ 11/4 → 3/1 | note:e s:saw ]", + "[ 3/1 → 13/4 | note:c s:saw ]", + "[ 13/4 → 7/2 | note:a s:saw ]", + "[ 7/2 → 15/4 | note:f s:saw ]", + "[ 15/4 → 4/1 | note:e s:saw ]", +] +`; + exports[`runs examples > example "midiport" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c midiport:0 ]", @@ -9064,6 +9186,43 @@ exports[`runs examples > example "ratio" example index 0 1`] = ` ] `; +exports[`runs examples > example "register" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:saw velocity:0 cutoff:0 ]", + "[ 1/8 → 1/4 | s:saw velocity:0.6852155700325966 cutoff:548.1724560260773 ]", + "[ 1/4 → 3/8 | s:saw velocity:0.36975969187915325 cutoff:295.8077535033226 ]", + "[ 3/8 → 1/2 | s:saw velocity:0.40139251574873924 cutoff:321.1140125989914 ]", + "[ 1/2 → 5/8 | s:saw velocity:0.2604806162416935 cutoff:208.3844929933548 ]", + "[ 5/8 → 3/4 | s:saw velocity:0.1356358677148819 cutoff:108.50869417190552 ]", + "[ 3/4 → 7/8 | s:saw velocity:0.19582648016512394 cutoff:156.66118413209915 ]", + "[ 7/8 → 1/1 | s:saw velocity:0.3976310808211565 cutoff:318.1048646569252 ]", + "[ 1/1 → 9/8 | s:saw velocity:0.5195421651005745 cutoff:415.6337320804596 ]", + "[ 9/8 → 5/4 | s:saw velocity:0.6724895145744085 cutoff:537.9916116595268 ]", + "[ 5/4 → 11/8 | s:saw velocity:0.7287282031029463 cutoff:582.982562482357 ]", + "[ 11/8 → 3/2 | s:saw velocity:0.18280375190079212 cutoff:146.2430015206337 ]", + "[ 3/2 → 13/8 | s:saw velocity:0.6084080748260021 cutoff:486.7264598608017 ]", + "[ 13/8 → 7/4 | s:saw velocity:0.49675471149384975 cutoff:397.4037691950798 ]", + "[ 7/4 → 15/8 | s:saw velocity:0.20473789609968662 cutoff:163.7903168797493 ]", + "[ 15/8 → 2/1 | s:saw velocity:0.5945571791380644 cutoff:475.6457433104515 ]", + "[ 2/1 → 17/8 | s:saw velocity:0.9595271199941635 cutoff:767.6216959953308 ]", + "[ 17/8 → 9/4 | s:saw velocity:0.9033902939409018 cutoff:722.7122351527214 ]", + "[ 9/4 → 19/8 | s:saw velocity:0.34548251144587994 cutoff:276.38600915670395 ]", + "[ 19/8 → 5/2 | s:saw velocity:0.8365066405385733 cutoff:669.2053124308586 ]", + "[ 5/2 → 21/8 | s:saw velocity:0.45861607417464256 cutoff:366.89285933971405 ]", + "[ 21/8 → 11/4 | s:saw velocity:0.16019348427653313 cutoff:128.1547874212265 ]", + "[ 11/4 → 23/8 | s:saw velocity:0.6332327704876661 cutoff:506.5862163901329 ]", + "[ 23/8 → 3/1 | s:saw velocity:0.01625417172908783 cutoff:13.003337383270264 ]", + "[ 3/1 → 25/8 | s:saw velocity:0.21728911064565182 cutoff:173.83128851652145 ]", + "[ 25/8 → 13/4 | s:saw velocity:0.34324387833476067 cutoff:274.59510266780853 ]", + "[ 13/4 → 27/8 | s:saw velocity:0.9923650715500116 cutoff:793.8920572400093 ]", + "[ 27/8 → 7/2 | s:saw velocity:0.40869180113077164 cutoff:326.9534409046173 ]", + "[ 7/2 → 29/8 | s:saw velocity:0.40994881466031075 cutoff:327.9590517282486 ]", + "[ 29/8 → 15/4 | s:saw velocity:0.9391485787928104 cutoff:751.3188630342484 ]", + "[ 15/4 → 31/8 | s:saw velocity:0.8121673222631216 cutoff:649.7338578104973 ]", + "[ 31/8 → 4/1 | s:saw velocity:0.7361665386706591 cutoff:588.9332309365273 ]", +] +`; + exports[`runs examples > example "release" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c3 release:0 ]", @@ -9685,42 +9844,6 @@ exports[`runs examples > example "samples" example index 1 1`] = ` ] `; -exports[`runs examples > example "samples" example index 2 1`] = ` -[ - "[ 0/1 → 1/2 | s:noise ]", - "[ 1/2 → 3/4 | s:chimp n:0 ]", - "[ 3/4 → 1/1 | s:chimp n:0 ]", - "[ 1/1 → 3/2 | s:noise ]", - "[ 3/2 → 2/1 | s:chimp n:1 ]", - "[ 2/1 → 5/2 | s:noise ]", - "[ 5/2 → 11/4 | s:chimp n:0 ]", - "[ 11/4 → 3/1 | s:chimp n:0 ]", - "[ 3/1 → 7/2 | s:noise ]", - "[ 7/2 → 4/1 | s:chimp n:1 ]", -] -`; - -exports[`runs examples > example "samples" example index 3 1`] = ` -[ - "[ 0/1 → 1/4 | s:chocolat ]", - "[ 1/4 → 1/2 | s:chocolat ]", - "[ 1/2 → 3/4 | s:chocolat ]", - "[ 3/4 → 1/1 | s:chocolat ]", - "[ 1/1 → 5/4 | s:chocolat ]", - "[ 5/4 → 3/2 | s:chocolat ]", - "[ 3/2 → 7/4 | s:chocolat ]", - "[ 7/4 → 2/1 | s:chocolat ]", - "[ 2/1 → 9/4 | s:chocolat ]", - "[ 9/4 → 5/2 | s:chocolat ]", - "[ 5/2 → 11/4 | s:chocolat ]", - "[ 11/4 → 3/1 | s:chocolat ]", - "[ 3/1 → 13/4 | s:chocolat ]", - "[ 13/4 → 7/2 | s:chocolat ]", - "[ 7/2 → 15/4 | s:chocolat ]", - "[ 15/4 → 4/1 | s:chocolat ]", -] -`; - exports[`runs examples > example "saw" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:c3 clip:0 ]", @@ -11725,6 +11848,75 @@ exports[`runs examples > example "sysexid" example index 0 1`] = ` ] `; +exports[`runs examples > example "tag" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:square note:F1 cutoff:40 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 1/16 → 1/8 | s:square note:F1 cutoff:41.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 1/8 → 3/16 | s:square note:F1 cutoff:42.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 3/16 → 1/4 | s:square note:F1 cutoff:43.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 1/4 → 5/16 | s:square note:F1 cutoff:45 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 5/16 → 3/8 | s:square note:F1 cutoff:46.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 3/8 → 7/16 | s:square note:F1 cutoff:47.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 7/16 → 1/2 | s:square note:F1 cutoff:48.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 1/2 → 9/16 | s:square note:F1 cutoff:50 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 9/16 → 5/8 | s:square note:F1 cutoff:51.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 5/8 → 11/16 | s:square note:F1 cutoff:52.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 11/16 → 3/4 | s:square note:F1 cutoff:53.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 3/4 → 13/16 | s:square note:F1 cutoff:55 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 13/16 → 7/8 | s:square note:F1 cutoff:56.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 7/8 → 15/16 | s:square note:F1 cutoff:57.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 15/16 → 1/1 | s:square note:F1 cutoff:58.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 1/1 → 17/16 | s:saw note:F2 cutoff:60 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 17/16 → 9/8 | s:saw note:F2 cutoff:61.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 9/8 → 19/16 | s:saw note:F2 cutoff:62.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 19/16 → 5/4 | s:saw note:F2 cutoff:63.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 5/4 → 21/16 | s:saw note:F2 cutoff:65 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 21/16 → 11/8 | s:saw note:F2 cutoff:66.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 11/8 → 23/16 | s:saw note:F2 cutoff:67.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 23/16 → 3/2 | s:saw note:F2 cutoff:68.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 3/2 → 25/16 | s:saw note:F2 cutoff:70 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 25/16 → 13/8 | s:saw note:F2 cutoff:71.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 13/8 → 27/16 | s:saw note:F2 cutoff:72.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 27/16 → 7/4 | s:saw note:F2 cutoff:73.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 7/4 → 29/16 | s:saw note:F2 cutoff:75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 29/16 → 15/8 | s:saw note:F2 cutoff:76.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 15/8 → 31/16 | s:saw note:F2 cutoff:77.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 31/16 → 2/1 | s:saw note:F2 cutoff:78.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 2/1 → 33/16 | s:saw note:F1 cutoff:80 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 33/16 → 17/8 | s:saw note:F1 cutoff:78.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 17/8 → 35/16 | s:saw note:F1 cutoff:77.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 35/16 → 9/4 | s:saw note:F1 cutoff:76.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 9/4 → 37/16 | s:saw note:F1 cutoff:75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 37/16 → 19/8 | s:saw note:F1 cutoff:73.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 19/8 → 39/16 | s:saw note:F1 cutoff:72.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 39/16 → 5/2 | s:saw note:F1 cutoff:71.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 5/2 → 41/16 | s:saw note:F1 cutoff:70 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 41/16 → 21/8 | s:saw note:F1 cutoff:68.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 21/8 → 43/16 | s:saw note:F1 cutoff:67.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 43/16 → 11/4 | s:saw note:F1 cutoff:66.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 11/4 → 45/16 | s:saw note:F1 cutoff:65 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 45/16 → 23/8 | s:saw note:F1 cutoff:63.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 23/8 → 47/16 | s:saw note:F1 cutoff:62.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 47/16 → 3/1 | s:saw note:F1 cutoff:61.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 3/1 → 49/16 | s:square note:F1 cutoff:60 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 49/16 → 25/8 | s:square note:F1 cutoff:58.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 25/8 → 51/16 | s:square note:F1 cutoff:57.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 51/16 → 13/4 | s:square note:F1 cutoff:56.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 13/4 → 53/16 | s:square note:F1 cutoff:55 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 53/16 → 27/8 | s:square note:F1 cutoff:53.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 27/8 → 55/16 | s:square note:F1 cutoff:52.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 55/16 → 7/2 | s:square note:F1 cutoff:51.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 7/2 → 57/16 | s:square note:F1 cutoff:50 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 57/16 → 29/8 | s:square note:F1 cutoff:48.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 29/8 → 59/16 | s:square note:F1 cutoff:47.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 59/16 → 15/4 | s:square note:F1 cutoff:46.25 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 15/4 → 61/16 | s:square note:F1 cutoff:45 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 61/16 → 31/8 | s:square note:F1 cutoff:43.75 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 31/8 → 63/16 | s:square note:F1 cutoff:42.5 lpenv:5 resonance:4 lpdecay:0.15 ]", + "[ 63/16 → 4/1 | s:square note:F1 cutoff:41.25 lpenv:5 resonance:4 lpdecay:0.15 ]", +] +`; + exports[`runs examples > example "take" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd ]", @@ -11837,6 +12029,84 @@ exports[`runs examples > example "tour" example index 0 1`] = ` ] `; +exports[`runs examples > example "transient" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:bd transient:-1 ]", + "[ 1/1 → 2/1 | s:bd transient:-0.5 ]", + "[ 2/1 → 3/1 | s:bd transient:0 ]", + "[ 3/1 → 4/1 | s:bd transient:0.5 ]", +] +`; + +exports[`runs examples > example "transient" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 1/16 → 1/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 1/8 → 3/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 3/16 → 1/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 1/4 → 5/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 5/16 → 3/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 3/8 → 7/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 7/16 → 1/2 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 1/2 → 9/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 9/16 → 5/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 5/8 → 11/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 11/16 → 3/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 3/4 → 13/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 13/16 → 7/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 7/8 → 15/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 15/16 → 1/1 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 1/1 → 17/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 17/16 → 9/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 9/8 → 19/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 19/16 → 5/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 5/4 → 21/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 21/16 → 11/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 11/8 → 23/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 23/16 → 3/2 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 3/2 → 25/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 25/16 → 13/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 13/8 → 27/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 27/16 → 7/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 7/4 → 29/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 29/16 → 15/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 15/8 → 31/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 31/16 → 2/1 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 2/1 → 33/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 33/16 → 17/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 17/8 → 35/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 35/16 → 9/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 9/4 → 37/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 37/16 → 19/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 19/8 → 39/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 39/16 → 5/2 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 5/2 → 41/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 41/16 → 21/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 21/8 → 43/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 43/16 → 11/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 11/4 → 45/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 45/16 → 23/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 23/8 → 47/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 47/16 → 3/1 | s:hh bank:tr909 transient:-1 transsustain:1 ]", + "[ 3/1 → 49/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 49/16 → 25/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 25/8 → 51/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 51/16 → 13/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 13/4 → 53/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 53/16 → 27/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 27/8 → 55/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 55/16 → 7/2 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 7/2 → 57/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 57/16 → 29/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 29/8 → 59/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 59/16 → 15/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 15/4 → 61/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 61/16 → 31/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 31/8 → 63/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]", + "[ 63/16 → 4/1 | s:hh bank:tr909 transient:1 transsustain:-1 ]", +] +`; + exports[`runs examples > example "transpose" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C2 ]",