Compare commits

...

18 Commits

Author SHA1 Message Date
Aria 48231ea12a Merge branch 'main' into glossing/midin-channels 2025-12-07 21:23:57 +01:00
Aria c8a442234e Merge pull request 'Feat: Transient shaper' (#1777) from glossing/transient-processor into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1777
2025-12-07 21:23:42 +01:00
Aria efda32eedb Merge branch 'main' into glossing/transient-processor 2025-12-07 20:29:50 +01:00
Aria 95d04d1149 Merge branch 'main' into glossing/midin-channels 2025-12-07 20:29:39 +01:00
Aria 358b1e3f56 Make sure processor turns off; fix array sizes for JIT 2025-12-07 13:28:57 -06:00
Aria fe7e8f7add Clearer docstring 2025-12-07 13:21:06 -06:00
Aria 45d329d3d1 Merge pull request 'Add vel as a synonym for velocity & update a few docstrings' (#1781) from glossing/controls-various into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1781
2025-12-07 19:15:42 +01:00
Aria fab8d2193d Merge branch 'main' into glossing/midin-channels 2025-12-07 18:45:16 +01:00
Aria 150835aa4d Merge branch 'main' into glossing/transient-processor 2025-12-07 18:45:11 +01:00
Aria c4826b7dfb Merge branch 'main' into glossing/midin-channels 2025-12-07 18:11:59 +01:00
Aria bd61c1dc7e Merge branch 'main' into glossing/transient-processor 2025-12-07 18:11:50 +01:00
Aria 36587ae747 Switch back to original version 2025-11-23 22:55:36 -06:00
Aria 6c01c16a0c Fix typo 2025-11-23 22:39:21 -06:00
Aria be36898803 Formatting and examples 2025-11-23 22:29:33 -06:00
Aria df047fd48d Optimizations 2025-11-23 22:28:10 -06:00
Aria 68840de188 Working version 2025-11-23 20:58:28 -06:00
Aria 020a6bc75f First pass at transient processor 2025-11-23 20:31:31 -06:00
Aria 7bc6fcbf2a Add channel support to midi in 2025-11-23 18:24:48 -06:00
5 changed files with 240 additions and 9 deletions
+14
View File
@@ -2786,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']);
+26 -9
View File
@@ -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;
+19
View File
@@ -448,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);
@@ -529,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));
+82
View File
@@ -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;
@@ -1384,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);
+99
View File
@@ -7048,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 ]",
@@ -12008,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 ]",