@denaudio/den 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +102 -2
- package/dist/biquad-eq.d.ts +11 -0
- package/dist/biquad-eq.js +35 -0
- package/dist/catalog-filter-math.d.ts +4 -0
- package/dist/catalog-filter-math.js +30 -0
- package/dist/character-filter.d.ts +13 -0
- package/dist/character-filter.js +53 -0
- package/dist/convolution-spectrum.d.ts +25 -0
- package/dist/convolution-spectrum.js +82 -0
- package/dist/convolution.d.ts +15 -0
- package/dist/convolution.js +81 -0
- package/dist/crossover.d.ts +11 -0
- package/dist/crossover.js +17 -0
- package/dist/curve-shaper.d.ts +26 -0
- package/dist/curve-shaper.js +73 -0
- package/dist/delay-fx.d.ts +37 -0
- package/dist/delay-fx.js +84 -0
- package/dist/delay-readhead.d.ts +19 -0
- package/dist/delay-readhead.js +54 -0
- package/dist/delay-settings.d.ts +55 -0
- package/dist/delay-settings.js +20 -0
- package/dist/drive.d.ts +24 -0
- package/dist/drive.js +116 -0
- package/dist/dual-head-delay.d.ts +28 -0
- package/dist/dual-head-delay.js +77 -0
- package/dist/dynamics.d.ts +61 -0
- package/dist/dynamics.js +130 -0
- package/dist/envelope.d.ts +34 -0
- package/dist/envelope.js +62 -0
- package/dist/filter.d.ts +9 -0
- package/dist/filter.js +34 -0
- package/dist/filterbank-vocoder.d.ts +35 -0
- package/dist/filterbank-vocoder.js +52 -0
- package/dist/formant-bank.d.ts +15 -0
- package/dist/formant-bank.js +28 -0
- package/dist/freeze-reverb.d.ts +35 -0
- package/dist/freeze-reverb.js +61 -0
- package/dist/frequency-shifter.d.ts +24 -0
- package/dist/frequency-shifter.js +57 -0
- package/dist/gate.d.ts +2 -0
- package/dist/gate.js +12 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/instrument-example.d.ts +12 -0
- package/dist/instrument-example.js +29 -0
- package/dist/instrument-settings.d.ts +95 -0
- package/dist/instrument-settings.js +36 -0
- package/dist/instrument.d.ts +42 -0
- package/dist/instrument.js +166 -0
- package/dist/lfo.d.ts +19 -0
- package/dist/lfo.js +55 -0
- package/dist/live-buffer.d.ts +39 -0
- package/dist/live-buffer.js +52 -0
- package/dist/live-granular.d.ts +41 -0
- package/dist/live-granular.js +64 -0
- package/dist/lookahead-limiter.d.ts +31 -0
- package/dist/lookahead-limiter.js +67 -0
- package/dist/loop-crossfade.d.ts +28 -0
- package/dist/loop-crossfade.js +72 -0
- package/dist/modulation-fx.d.ts +47 -0
- package/dist/modulation-fx.js +58 -0
- package/dist/modulation.d.ts +132 -0
- package/dist/modulation.js +228 -0
- package/dist/mpe-expression.d.ts +36 -0
- package/dist/mpe-expression.js +73 -0
- package/dist/multiband-dynamics.d.ts +50 -0
- package/dist/multiband-dynamics.js +60 -0
- package/dist/musical-controls.d.ts +76 -0
- package/dist/musical-controls.js +125 -0
- package/dist/musical-examples.d.ts +21 -0
- package/dist/musical-examples.js +142 -0
- package/dist/musical-materials.d.ts +110 -0
- package/dist/musical-materials.js +56 -0
- package/dist/oscillator.d.ts +10 -0
- package/dist/oscillator.js +35 -0
- package/dist/oversampled-drive.d.ts +19 -0
- package/dist/oversampled-drive.js +99 -0
- package/dist/performance.d.ts +61 -0
- package/dist/performance.js +241 -0
- package/dist/pitch-quantizer.d.ts +26 -0
- package/dist/pitch-quantizer.js +88 -0
- package/dist/prepared-convolution.d.ts +22 -0
- package/dist/prepared-convolution.js +99 -0
- package/dist/resident-recorder.d.ts +28 -0
- package/dist/resident-recorder.js +68 -0
- package/dist/resident-time-stretch.d.ts +44 -0
- package/dist/resident-time-stretch.js +135 -0
- package/dist/resonator.d.ts +32 -0
- package/dist/resonator.js +63 -0
- package/dist/reverb.d.ts +27 -0
- package/dist/reverb.js +52 -0
- package/dist/sample.d.ts +113 -0
- package/dist/sample.js +212 -0
- package/dist/source.d.ts +58 -0
- package/dist/source.js +113 -0
- package/dist/spatial-chains.d.ts +48 -0
- package/dist/spatial-chains.js +76 -0
- package/dist/spectral-fft.d.ts +21 -0
- package/dist/spectral-fft.js +74 -0
- package/dist/spectral-freeze.d.ts +22 -0
- package/dist/spectral-freeze.js +74 -0
- package/dist/spectral-gate.d.ts +21 -0
- package/dist/spectral-gate.js +58 -0
- package/dist/spectral-stft.d.ts +13 -0
- package/dist/spectral-stft.js +69 -0
- package/dist/spectral-texture.d.ts +42 -0
- package/dist/spectral-texture.js +141 -0
- package/dist/spectral.d.ts +16 -0
- package/dist/spectral.js +10 -0
- package/dist/state-variable-filter.d.ts +17 -0
- package/dist/state-variable-filter.js +25 -0
- package/dist/stereo-delay.d.ts +45 -0
- package/dist/stereo-delay.js +70 -0
- package/dist/virtual-analog.d.ts +26 -0
- package/dist/virtual-analog.js +66 -0
- package/dist/voice-policy.d.ts +37 -0
- package/dist/voice-policy.js +245 -0
- package/dist/wavetable-bands.d.ts +37 -0
- package/dist/wavetable-bands.js +145 -0
- package/dist/wavetable-read.d.ts +6 -0
- package/dist/wavetable-read.js +11 -0
- package/dist/wavetable.d.ts +29 -0
- package/dist/wavetable.js +49 -0
- package/dist/windowed-pitch-shift.d.ts +34 -0
- package/dist/windowed-pitch-shift.js +51 -0
- package/docs/catalog-audition.md +76 -0
- package/docs/catalog-filters.md +125 -0
- package/docs/catalog-modulation-fx.md +220 -0
- package/docs/catalog-status.md +431 -0
- package/docs/character-filter.md +139 -0
- package/docs/contracts.md +131 -0
- package/docs/control-modulation.md +194 -0
- package/docs/convolution.md +45 -0
- package/docs/curve-shaper.md +117 -0
- package/docs/delay-fx-capture-findings.json +134 -0
- package/docs/delay-fx.md +202 -0
- package/docs/delay-readhead.md +121 -0
- package/docs/delay-settings-candidate.json +333 -0
- package/docs/delay-settings.md +111 -0
- package/docs/dependency-findings.md +61 -0
- package/docs/deployment-status.md +37 -0
- package/docs/drive.md +152 -0
- package/docs/dual-head-delay.md +131 -0
- package/docs/dynamics.md +180 -0
- package/docs/envelope-integer-staging.md +64 -0
- package/docs/filter-materialization-results.json +71 -0
- package/docs/filter-materialization.md +135 -0
- package/docs/filter-oscillator.md +126 -0
- package/docs/filterbank-vocoder-entry.md +58 -0
- package/docs/freeze-reverb.md +179 -0
- package/docs/frequency-shifter.md +133 -0
- package/docs/frontend-audition.md +22 -0
- package/docs/gain-schedule-verification.md +53 -0
- package/docs/initial-acceptance.md +79 -0
- package/docs/instrument-performance.md +191 -0
- package/docs/instrument-settings.md +130 -0
- package/docs/instrument.md +204 -0
- package/docs/integration-candidate.md +114 -0
- package/docs/live-buffer.md +163 -0
- package/docs/live-granular.md +179 -0
- package/docs/lookahead-multiband.md +183 -0
- package/docs/loop-crossfade.md +99 -0
- package/docs/mpe-expression.md +65 -0
- package/docs/musical-clock-seek-boundary.md +31 -0
- package/docs/musical-controls-contract.md +144 -0
- package/docs/musical-examples.md +255 -0
- package/docs/oversampled-drive.md +153 -0
- package/docs/performance.md +53 -0
- package/docs/pitch-quantizer.md +55 -0
- package/docs/prepared-convolution.md +188 -0
- package/docs/realtime-audition.md +63 -0
- package/docs/resident-recorder.md +151 -0
- package/docs/resident-time-stretch.md +190 -0
- package/docs/sample.md +191 -0
- package/docs/sound-audition-headroom.md +103 -0
- package/docs/sources.md +81 -0
- package/docs/spatial-chains-entry.md +250 -0
- package/docs/spectral-freeze-entry.md +130 -0
- package/docs/spectral-gate-entry.md +130 -0
- package/docs/spectral-large-candidate.md +74 -0
- package/docs/spectral-texture-entry.md +174 -0
- package/docs/spectral.md +118 -0
- package/docs/virtual-analog.md +77 -0
- package/docs/voice-policy.md +38 -0
- package/docs/wavetable-bands.md +138 -0
- package/docs/wavetable.md +101 -0
- package/docs/windowed-pitch-shift.md +112 -0
- package/package.json +269 -4
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { defineSubgraph, f32, f64, i32, select, state } from '@unworklet/core';
|
|
2
|
+
// Exact binary representation scaling, not audio normalization. Keeps the
|
|
3
|
+
// smallest f32 excitations above the native tiny-state scrub threshold.
|
|
4
|
+
const SCALE = 2 ** 256;
|
|
5
|
+
const ROWS = [[1, 1, 1, 1], [1, -1, 1, -1], [1, 1, -1, -1], [1, -1, -1, 1]];
|
|
6
|
+
/** Wet-only four-line integer-delay FDN. Call tick exactly once per sample.
|
|
7
|
+
* The fully frozen, unforced real-arithmetic network preserves stored energy.
|
|
8
|
+
* Actual f64 state has rounding and very-small-state limits, not perpetual
|
|
9
|
+
* exact energy. Finite normalized inputs and adequate output headroom required.
|
|
10
|
+
* No interpolation, damping filter, shimmer, limiter, dry path or normalization.
|
|
11
|
+
*/
|
|
12
|
+
export const freezeReverb = defineSubgraph((config) => {
|
|
13
|
+
const { sampleRate, roomScale = 1, decaySeconds = 1.5, transitionSamples = 256 } = config;
|
|
14
|
+
if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 192000 ||
|
|
15
|
+
!Number.isFinite(roomScale) || roomScale < 0.5 || roomScale > 2 ||
|
|
16
|
+
!Number.isFinite(decaySeconds) || decaySeconds < 0.1 || decaySeconds > 10 ||
|
|
17
|
+
!Number.isInteger(transitionSamples) || transitionSamples < 0 || transitionSamples > 192000) {
|
|
18
|
+
throw new RangeError('freezeReverb requires integer sampleRate [8000,192000], roomScale [0.5,2], decaySeconds [0.1,10], integer transitionSamples [0,192000]');
|
|
19
|
+
}
|
|
20
|
+
const steps = Math.max(1, transitionSamples);
|
|
21
|
+
const progress = state.i32(0).named('freezeProgress');
|
|
22
|
+
const lines = [0.0297, 0.0371, 0.0411, 0.0437].map((seconds, index) => {
|
|
23
|
+
const size = Math.round(seconds * roomScale * sampleRate);
|
|
24
|
+
return {
|
|
25
|
+
size,
|
|
26
|
+
gain: 10 ** (-3 * size / (sampleRate * decaySeconds)),
|
|
27
|
+
history: state.buffer.f64({ size }).expose({ name: `line${index}/historyScaled`, snapshot: 'persistent' }),
|
|
28
|
+
cursor: state.i32(0).named(`line${index}/cursor`),
|
|
29
|
+
valid: state.i32(0).named(`line${index}/valid`),
|
|
30
|
+
wet: state.f64(0).expose({ name: `line${index}/wetScaled`, snapshot: 'transient' }),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
tick(inputLeft, inputRight, c) {
|
|
35
|
+
const next = transitionSamples === 0
|
|
36
|
+
? select(c.freeze, i32(1), i32(0))
|
|
37
|
+
: select(c.freeze, select(progress.read().lt(steps), progress.read().add(1), i32(steps)), select(progress.read().gt(0), progress.read().sub(1), i32(0)));
|
|
38
|
+
progress.write(select(c.reset, i32(0), next));
|
|
39
|
+
const amount = f64(progress.read()).div(steps);
|
|
40
|
+
const excitation = f64(1).sub(amount);
|
|
41
|
+
// Materialize every outgoing sample before any buffer write. Integer
|
|
42
|
+
// cursors avoid the fractional seconds/interpolation of delayReadhead.
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
line.wet.write(select(c.reset.or(line.valid.read().lt(line.size)), f64(0), line.history.read(line.cursor.read())));
|
|
45
|
+
}
|
|
46
|
+
const returns = lines.map(line => line.wet.read().mul(amount.add(excitation.mul(line.gain))));
|
|
47
|
+
const left = select(c.reset, f64(0), f64(inputLeft).mul(SCALE).mul(excitation));
|
|
48
|
+
const right = select(c.reset, f64(0), f64(inputRight).mul(SCALE).mul(excitation));
|
|
49
|
+
for (let row = 0; row < lines.length; row++) {
|
|
50
|
+
const line = lines[row];
|
|
51
|
+
const feedback = returns.reduce((sum, value, column) => sum.add(value.mul(ROWS[row][column] * 0.5)), f64(0));
|
|
52
|
+
const injection = left.add(right.mul(ROWS[1][row])).mul(0.5);
|
|
53
|
+
line.history.write(select(c.reset, i32(0), line.cursor.read()), injection.add(feedback));
|
|
54
|
+
line.cursor.write(select(c.reset, i32(0), select(line.cursor.read().eq(line.size - 1), i32(0), line.cursor.read().add(1))));
|
|
55
|
+
line.valid.write(select(c.reset, i32(0), select(line.valid.read().lt(line.size), line.valid.read().add(1), i32(line.size))));
|
|
56
|
+
}
|
|
57
|
+
const output = (row) => f32(lines.reduce((sum, line, column) => sum.add(line.wet.read().mul(ROWS[row][column] * 0.5)), f64(0)).div(SCALE));
|
|
58
|
+
return { left: output(2), right: output(3), freezeAmount: f32(amount), frozen: progress.read().eq(steps) };
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Node } from '@unworklet/core';
|
|
2
|
+
/** Matched real/Hilbert group delay; the FIR transient spans 63 samples. */
|
|
3
|
+
export declare const FREQUENCY_SHIFTER_LATENCY_SAMPLES = 31;
|
|
4
|
+
export interface FrequencyShifterConfig {
|
|
5
|
+
sampleRate: number;
|
|
6
|
+
}
|
|
7
|
+
export interface FrequencyShifterControls {
|
|
8
|
+
/** Signed Hz, clamped to [-sampleRate/4, sampleRate/4]. Zero holds phase. */
|
|
9
|
+
shiftHz: Node<'f32'>;
|
|
10
|
+
/** Linear, latency-aligned dry/wet blend, clamped to [0,1]. */
|
|
11
|
+
mix: Node<'f32'>;
|
|
12
|
+
/** Select delayed dry; history and oscillator continue running. */
|
|
13
|
+
bypass: Node<'bool'>;
|
|
14
|
+
/** Level reset: silence/discard this sample, clear history and phase. */
|
|
15
|
+
reset: Node<'bool'>;
|
|
16
|
+
}
|
|
17
|
+
/** Fixed 63-tap Blackman Hilbert frequency shifter. Call once per sample.
|
|
18
|
+
* Finite controls and normalized finite input [-1,1] required. Output may exceed
|
|
19
|
+
* unity. Approximate single-sideband behavior only in the documented useful
|
|
20
|
+
* band; there is no input bandpass, antialiasing, pitch-ratio or time stretching.
|
|
21
|
+
*/
|
|
22
|
+
export declare const frequencyShifter: import("@unworklet/core").SubgraphDecl<[config: FrequencyShifterConfig], {
|
|
23
|
+
tick(input: Node<"f32">, c: FrequencyShifterControls): Node<"f32">;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { defineSubgraph, f32, f64, i32, select, state } from '@unworklet/core';
|
|
2
|
+
/** Matched real/Hilbert group delay; the FIR transient spans 63 samples. */
|
|
3
|
+
export const FREQUENCY_SHIFTER_LATENCY_SAMPLES = 31;
|
|
4
|
+
// Full-cycle degree-15 sine with f64 arithmetic and a <7e-12 remainder.
|
|
5
|
+
function sine(cycles) {
|
|
6
|
+
const p = cycles.sub(cycles.add(0.5).floor());
|
|
7
|
+
const folded = select(p.gt(0.25), f64(0.5).sub(p), select(p.lt(-0.25), f64(-0.5).sub(p), p));
|
|
8
|
+
const angle = folded.mul(2 * Math.PI), square = angle.mul(angle);
|
|
9
|
+
return angle.mul(f64(-1 / 1307674368000).mul(square).add(1 / 6227020800).mul(square).sub(1 / 39916800)
|
|
10
|
+
.mul(square).add(1 / 362880).mul(square).sub(1 / 5040)
|
|
11
|
+
.mul(square).add(1 / 120).mul(square).sub(1 / 6).mul(square).add(1));
|
|
12
|
+
}
|
|
13
|
+
/** Fixed 63-tap Blackman Hilbert frequency shifter. Call once per sample.
|
|
14
|
+
* Finite controls and normalized finite input [-1,1] required. Output may exceed
|
|
15
|
+
* unity. Approximate single-sideband behavior only in the documented useful
|
|
16
|
+
* band; there is no input bandpass, antialiasing, pitch-ratio or time stretching.
|
|
17
|
+
*/
|
|
18
|
+
export const frequencyShifter = defineSubgraph((config) => {
|
|
19
|
+
const { sampleRate } = config;
|
|
20
|
+
if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 192000) {
|
|
21
|
+
throw new RangeError('frequencyShifter requires an integer sampleRate in [8000,192000]');
|
|
22
|
+
}
|
|
23
|
+
const delay = FREQUENCY_SHIFTER_LATENCY_SAMPLES, size = 63;
|
|
24
|
+
// Public 0.4.1 scrubs buffer writes below 1e-30. Exact power-of-two scaling
|
|
25
|
+
// preserves the full finite normalized f32 input range, including subnormals.
|
|
26
|
+
const scale = 2 ** 128;
|
|
27
|
+
const history = state.buffer.f64({ size }).expose({ name: 'historyScaled', snapshot: 'persistent' });
|
|
28
|
+
const cursor = state.i32(0).named('cursor');
|
|
29
|
+
const valid = state.i32(0).named('valid');
|
|
30
|
+
// Centered scaled phase also preserves tiny signed increments near zero.
|
|
31
|
+
// As with any f64 accumulator, increments below its current ULP can stall.
|
|
32
|
+
const phase = state.f64(0).named('phaseScaled');
|
|
33
|
+
const quadrature = state.f64(0).expose({ name: 'quadratureScaled', snapshot: 'transient' });
|
|
34
|
+
return {
|
|
35
|
+
tick(input, c) {
|
|
36
|
+
valid.write(select(c.reset, i32(0), valid.read().add(1).min(size)));
|
|
37
|
+
history.write(cursor.read(), f64(select(c.reset, f32(0), input)).mul(scale));
|
|
38
|
+
const at = (lag) => select(valid.read().gt(lag), history.read(cursor.read().add(size - lag).mod(size)), f64(0));
|
|
39
|
+
quadrature.write(0);
|
|
40
|
+
// Odd antisymmetric taps. The Blackman endpoints are exactly zero;
|
|
41
|
+
// pair differences reduce the 63-tap FIR to 15 bounded multiply-adds.
|
|
42
|
+
for (let offset = 1; offset < delay; offset += 2) {
|
|
43
|
+
const window = 0.42 + 0.5 * Math.cos(Math.PI * offset / delay) + 0.08 * Math.cos(2 * Math.PI * offset / delay);
|
|
44
|
+
quadrature.write(quadrature.read().add(at(delay + offset).sub(at(delay - offset)).mul(2 * window / (Math.PI * offset))));
|
|
45
|
+
}
|
|
46
|
+
const dry = at(delay), p = select(c.reset, f64(0), phase.read().div(scale));
|
|
47
|
+
const wet = dry.mul(sine(p.add(0.25))).sub(quadrature.read().mul(sine(p)));
|
|
48
|
+
const mix = f64(c.mix).clamp(0, 1);
|
|
49
|
+
const output = select(c.bypass, dry, dry.mul(f64(1).sub(mix)).add(wet.mul(mix)));
|
|
50
|
+
const next = p.add(f64(c.shiftHz).clamp(-sampleRate / 4, sampleRate / 4).div(sampleRate));
|
|
51
|
+
// A held reset holds phase zero; the first subsequent sample uses zero.
|
|
52
|
+
phase.write(select(c.reset, f64(0), next.sub(next.add(0.5).floor()).mul(scale)));
|
|
53
|
+
cursor.write(cursor.read().add(1).mod(size));
|
|
54
|
+
return select(c.reset, f32(0), f32(output.div(scale)));
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
});
|
package/dist/gate.d.ts
ADDED
package/dist/gate.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { audioInput, audioOutput, defineProcessor, forSample, instantiate, param } from '@unworklet/core';
|
|
2
|
+
import { gateCell } from './index.js';
|
|
3
|
+
/** One-sample memory with gain: only an entry-path verification fixture. */
|
|
4
|
+
export const gate = defineProcessor(() => {
|
|
5
|
+
const input = audioInput({ channels: 1, name: 'main' });
|
|
6
|
+
const output = audioOutput({ channels: 1, name: 'main' });
|
|
7
|
+
const gain = param.f32({ default: 0.5, min: 0, max: 1, automationRate: 'a-rate' }).named('gain');
|
|
8
|
+
const cell = instantiate(gateCell, { scale: 1 }, { name: 'cell' });
|
|
9
|
+
return { process() {
|
|
10
|
+
forSample(i => output.ch(0).at(i).write(cell.tick(input.ch(0).at(i), gain.at(i))));
|
|
11
|
+
} };
|
|
12
|
+
}, { id: 'den.entry.gate.v1' });
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Node } from '@unworklet/core';
|
|
2
|
+
/** Minimal composition fixture, not a production DSP module. Required config avoids upstream #104. */
|
|
3
|
+
export declare const gateCell: import("@unworklet/core").SubgraphDecl<[config: {
|
|
4
|
+
scale: number;
|
|
5
|
+
}], {
|
|
6
|
+
tick(input: Node<"f32">, gain: Node<"f32">): Node<"f32">;
|
|
7
|
+
}>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineSubgraph, state } from '@unworklet/core';
|
|
2
|
+
/** Minimal composition fixture, not a production DSP module. Required config avoids upstream #104. */
|
|
3
|
+
export const gateCell = defineSubgraph((config) => {
|
|
4
|
+
const previous = state.f32(0).named('previous');
|
|
5
|
+
return {
|
|
6
|
+
tick(input, gain) {
|
|
7
|
+
const output = previous.read().mul(gain).mul(config.scale);
|
|
8
|
+
previous.write(input);
|
|
9
|
+
return output;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Node } from '@unworklet/core';
|
|
2
|
+
import type { OscillatorConfig } from './oscillator.js';
|
|
3
|
+
import type { FilterConfig } from './filter.js';
|
|
4
|
+
/** Replacement demonstration: half-level sine using unworklet's math primitive. */
|
|
5
|
+
export declare const exampleOscillator: import("@unworklet/core").SubgraphDecl<[config: OscillatorConfig], {
|
|
6
|
+
tick(frequency: Node<"f32">, reset: Node<"bool">): Node<"f32">;
|
|
7
|
+
}>;
|
|
8
|
+
/** Replacement demonstration: one-pole low-pass, intentionally ignores Q. */
|
|
9
|
+
export declare const exampleFilter: import("@unworklet/core").SubgraphDecl<[config: FilterConfig], {
|
|
10
|
+
tick(input: Node<"f32">, cutoff: Node<"f32">, _q: Node<"f32">, reset: Node<"bool">): Node<"f32">;
|
|
11
|
+
}>;
|
|
12
|
+
export declare const replacementInstrument: import("@unworklet/core").CompiledProcessor<unknown>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineSubgraph, f32, f64, select, state } from '@unworklet/core';
|
|
2
|
+
import { createInstrument } from './instrument.js';
|
|
3
|
+
/** Replacement demonstration: half-level sine using unworklet's math primitive. */
|
|
4
|
+
export const exampleOscillator = defineSubgraph((config) => {
|
|
5
|
+
if (config.waveform !== 'sine')
|
|
6
|
+
throw new RangeError('exampleOscillator supports sine only');
|
|
7
|
+
const phase = state.f64(0).named('phase');
|
|
8
|
+
return { tick(frequency, reset) {
|
|
9
|
+
const p = select(reset, f64(0), phase.read());
|
|
10
|
+
phase.write(p.add(f64(frequency.clamp(0, 0.45 * config.sampleRate)).div(config.sampleRate)).frac());
|
|
11
|
+
return f32(p.mul(2 * Math.PI).sin()).mul(0.5);
|
|
12
|
+
} };
|
|
13
|
+
});
|
|
14
|
+
/** Replacement demonstration: one-pole low-pass, intentionally ignores Q. */
|
|
15
|
+
export const exampleFilter = defineSubgraph((config) => {
|
|
16
|
+
const previous = state.f64(0).named('previous');
|
|
17
|
+
return { tick(input, cutoff, _q, reset) {
|
|
18
|
+
// Backward Euler one-pole: alpha = w/(1+w), w=2*pi*fc/fs.
|
|
19
|
+
const w = f64(cutoff.clamp(20, Math.min(20000, 0.45 * config.sampleRate))).mul(2 * Math.PI / config.sampleRate);
|
|
20
|
+
const old = select(reset, f64(0), previous.read());
|
|
21
|
+
const output = old.add(w.div(w.add(1)).mul(f64(input).sub(old)));
|
|
22
|
+
previous.write(output);
|
|
23
|
+
return f32(output);
|
|
24
|
+
} };
|
|
25
|
+
});
|
|
26
|
+
export const replacementInstrument = createInstrument({
|
|
27
|
+
mode: 'poly', capacity: 4, heldCapacity: 32, waveform: 'sine',
|
|
28
|
+
oscillator: exampleOscillator, filter: exampleFilter,
|
|
29
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** CANDIDATE: low saw body, brief filter transient, last-held mono legato. */
|
|
2
|
+
export declare const bassConfig: {
|
|
3
|
+
readonly mode: "mono";
|
|
4
|
+
readonly capacity: 1;
|
|
5
|
+
readonly heldCapacity: 32;
|
|
6
|
+
readonly legato: true;
|
|
7
|
+
readonly waveform: "saw";
|
|
8
|
+
};
|
|
9
|
+
export declare const bassParameters: {
|
|
10
|
+
readonly gain: 0.12;
|
|
11
|
+
readonly ampAttack: 0.005;
|
|
12
|
+
readonly ampDecay: 0.12;
|
|
13
|
+
readonly ampSustain: 0.7;
|
|
14
|
+
readonly ampRelease: 0.12;
|
|
15
|
+
readonly pitchAttack: 0;
|
|
16
|
+
readonly pitchDecay: 0;
|
|
17
|
+
readonly pitchSustain: 0;
|
|
18
|
+
readonly pitchRelease: 0;
|
|
19
|
+
readonly filterAttack: 0.003;
|
|
20
|
+
readonly filterDecay: 0.18;
|
|
21
|
+
readonly filterSustain: 0.2;
|
|
22
|
+
readonly filterRelease: 0.12;
|
|
23
|
+
readonly cutoff: 300;
|
|
24
|
+
readonly resonance: 0.707;
|
|
25
|
+
readonly pitchEnvelopeDepth: 0;
|
|
26
|
+
readonly filterEnvelopeDepth: 3;
|
|
27
|
+
readonly lfoRate: 0;
|
|
28
|
+
readonly lfoAmpDepth: 0;
|
|
29
|
+
readonly lfoPitchDepth: 0;
|
|
30
|
+
readonly lfoFilterDepth: 0;
|
|
31
|
+
readonly bypass: 0;
|
|
32
|
+
};
|
|
33
|
+
/** CANDIDATE: tonal kick/tom body, two-octave pitch drop, retriggered hits. */
|
|
34
|
+
export declare const percussionConfig: {
|
|
35
|
+
readonly mode: "mono";
|
|
36
|
+
readonly capacity: 1;
|
|
37
|
+
readonly heldCapacity: 32;
|
|
38
|
+
readonly legato: false;
|
|
39
|
+
readonly waveform: "sine";
|
|
40
|
+
};
|
|
41
|
+
export declare const percussionParameters: {
|
|
42
|
+
readonly gain: 0.16;
|
|
43
|
+
readonly ampAttack: 0.001;
|
|
44
|
+
readonly ampDecay: 0.22;
|
|
45
|
+
readonly ampSustain: 0;
|
|
46
|
+
readonly ampRelease: 0.04;
|
|
47
|
+
readonly pitchAttack: 0;
|
|
48
|
+
readonly pitchDecay: 0.06;
|
|
49
|
+
readonly pitchSustain: 0;
|
|
50
|
+
readonly pitchRelease: 0.04;
|
|
51
|
+
readonly filterAttack: 0;
|
|
52
|
+
readonly filterDecay: 0.08;
|
|
53
|
+
readonly filterSustain: 0;
|
|
54
|
+
readonly filterRelease: 0.04;
|
|
55
|
+
readonly cutoff: 800;
|
|
56
|
+
readonly resonance: 0.5;
|
|
57
|
+
readonly pitchEnvelopeDepth: 24;
|
|
58
|
+
readonly filterEnvelopeDepth: 2;
|
|
59
|
+
readonly lfoRate: 0;
|
|
60
|
+
readonly lfoAmpDepth: 0;
|
|
61
|
+
readonly lfoPitchDepth: 0;
|
|
62
|
+
readonly lfoFilterDepth: 0;
|
|
63
|
+
readonly bypass: 0;
|
|
64
|
+
};
|
|
65
|
+
/** CANDIDATE: four saw voices, slow rise/release and shared slow modulation. */
|
|
66
|
+
export declare const padConfig: {
|
|
67
|
+
readonly mode: "poly";
|
|
68
|
+
readonly capacity: 4;
|
|
69
|
+
readonly heldCapacity: 32;
|
|
70
|
+
readonly waveform: "saw";
|
|
71
|
+
};
|
|
72
|
+
export declare const padParameters: {
|
|
73
|
+
readonly gain: 0.045;
|
|
74
|
+
readonly ampAttack: 0.8;
|
|
75
|
+
readonly ampDecay: 1.2;
|
|
76
|
+
readonly ampSustain: 0.7;
|
|
77
|
+
readonly ampRelease: 2.4;
|
|
78
|
+
readonly pitchAttack: 0;
|
|
79
|
+
readonly pitchDecay: 0;
|
|
80
|
+
readonly pitchSustain: 0;
|
|
81
|
+
readonly pitchRelease: 0;
|
|
82
|
+
readonly filterAttack: 1.4;
|
|
83
|
+
readonly filterDecay: 1.6;
|
|
84
|
+
readonly filterSustain: 0.5;
|
|
85
|
+
readonly filterRelease: 2.4;
|
|
86
|
+
readonly cutoff: 1400;
|
|
87
|
+
readonly resonance: 0.5;
|
|
88
|
+
readonly pitchEnvelopeDepth: 0;
|
|
89
|
+
readonly filterEnvelopeDepth: 0.75;
|
|
90
|
+
readonly lfoRate: 0.12;
|
|
91
|
+
readonly lfoAmpDepth: 0.08;
|
|
92
|
+
readonly lfoPitchDepth: 0.03;
|
|
93
|
+
readonly lfoFilterDepth: 0.2;
|
|
94
|
+
readonly bypass: 0;
|
|
95
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** CANDIDATE: low saw body, brief filter transient, last-held mono legato. */
|
|
2
|
+
export const bassConfig = {
|
|
3
|
+
mode: 'mono', capacity: 1, heldCapacity: 32, legato: true, waveform: 'saw',
|
|
4
|
+
};
|
|
5
|
+
export const bassParameters = {
|
|
6
|
+
gain: 0.12,
|
|
7
|
+
ampAttack: 0.005, ampDecay: 0.12, ampSustain: 0.7, ampRelease: 0.12,
|
|
8
|
+
pitchAttack: 0, pitchDecay: 0, pitchSustain: 0, pitchRelease: 0,
|
|
9
|
+
filterAttack: 0.003, filterDecay: 0.18, filterSustain: 0.2, filterRelease: 0.12,
|
|
10
|
+
cutoff: 300, resonance: 0.707, pitchEnvelopeDepth: 0, filterEnvelopeDepth: 3,
|
|
11
|
+
lfoRate: 0, lfoAmpDepth: 0, lfoPitchDepth: 0, lfoFilterDepth: 0, bypass: 0,
|
|
12
|
+
};
|
|
13
|
+
/** CANDIDATE: tonal kick/tom body, two-octave pitch drop, retriggered hits. */
|
|
14
|
+
export const percussionConfig = {
|
|
15
|
+
mode: 'mono', capacity: 1, heldCapacity: 32, legato: false, waveform: 'sine',
|
|
16
|
+
};
|
|
17
|
+
export const percussionParameters = {
|
|
18
|
+
gain: 0.16,
|
|
19
|
+
ampAttack: 0.001, ampDecay: 0.22, ampSustain: 0, ampRelease: 0.04,
|
|
20
|
+
pitchAttack: 0, pitchDecay: 0.06, pitchSustain: 0, pitchRelease: 0.04,
|
|
21
|
+
filterAttack: 0, filterDecay: 0.08, filterSustain: 0, filterRelease: 0.04,
|
|
22
|
+
cutoff: 800, resonance: 0.5, pitchEnvelopeDepth: 24, filterEnvelopeDepth: 2,
|
|
23
|
+
lfoRate: 0, lfoAmpDepth: 0, lfoPitchDepth: 0, lfoFilterDepth: 0, bypass: 0,
|
|
24
|
+
};
|
|
25
|
+
/** CANDIDATE: four saw voices, slow rise/release and shared slow modulation. */
|
|
26
|
+
export const padConfig = {
|
|
27
|
+
mode: 'poly', capacity: 4, heldCapacity: 32, waveform: 'saw',
|
|
28
|
+
};
|
|
29
|
+
export const padParameters = {
|
|
30
|
+
gain: 0.045,
|
|
31
|
+
ampAttack: 0.8, ampDecay: 1.2, ampSustain: 0.7, ampRelease: 2.4,
|
|
32
|
+
pitchAttack: 0, pitchDecay: 0, pitchSustain: 0, pitchRelease: 0,
|
|
33
|
+
filterAttack: 1.4, filterDecay: 1.6, filterSustain: 0.5, filterRelease: 2.4,
|
|
34
|
+
cutoff: 1400, resonance: 0.5, pitchEnvelopeDepth: 0, filterEnvelopeDepth: 0.75,
|
|
35
|
+
lfoRate: 0.12, lfoAmpDepth: 0.08, lfoPitchDepth: 0.03, lfoFilterDepth: 0.2, bypass: 0,
|
|
36
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { oscillator } from './oscillator.js';
|
|
2
|
+
import { filter } from './filter.js';
|
|
3
|
+
import { type VoicePolicyConfig } from './voice-policy.js';
|
|
4
|
+
export { bassConfig, bassParameters, percussionConfig, percussionParameters, padConfig, padParameters } from './instrument-settings.js';
|
|
5
|
+
export interface InstrumentConfig extends VoicePolicyConfig {
|
|
6
|
+
waveform?: 'sine' | 'saw';
|
|
7
|
+
/** Compatible unworklet subgraphs, instantiated independently for each voice. */
|
|
8
|
+
oscillator?: typeof oscillator;
|
|
9
|
+
filter?: typeof filter;
|
|
10
|
+
}
|
|
11
|
+
/** Diagnostic initial AudioParam values, not an approved preset or a preset format. */
|
|
12
|
+
export declare const diagnosticInstrumentParameters: {
|
|
13
|
+
readonly gain: 0.15;
|
|
14
|
+
readonly ampAttack: 0.005;
|
|
15
|
+
readonly ampDecay: 0.08;
|
|
16
|
+
readonly ampSustain: 0.7;
|
|
17
|
+
readonly ampRelease: 0.15;
|
|
18
|
+
readonly pitchAttack: 0;
|
|
19
|
+
readonly pitchDecay: 0.1;
|
|
20
|
+
readonly pitchSustain: 0;
|
|
21
|
+
readonly pitchRelease: 0.1;
|
|
22
|
+
readonly filterAttack: 0.01;
|
|
23
|
+
readonly filterDecay: 0.15;
|
|
24
|
+
readonly filterSustain: 0.2;
|
|
25
|
+
readonly filterRelease: 0.15;
|
|
26
|
+
readonly cutoff: 2500;
|
|
27
|
+
readonly resonance: 0.707;
|
|
28
|
+
readonly pitchEnvelopeDepth: 0;
|
|
29
|
+
readonly filterEnvelopeDepth: 1;
|
|
30
|
+
readonly lfoRate: 2;
|
|
31
|
+
readonly lfoAmpDepth: 0;
|
|
32
|
+
readonly lfoPitchDepth: 0;
|
|
33
|
+
readonly lfoFilterDepth: 0;
|
|
34
|
+
readonly bypass: 0;
|
|
35
|
+
};
|
|
36
|
+
/** MIDI input `midi`, stereo dual-mono output `main`, panic message `reset`.
|
|
37
|
+
* Capacities/mode/waveform/parts are construction choices; all musical controls
|
|
38
|
+
* use native AudioParams. See docs/instrument.md for timing and reset semantics.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createInstrument(config: InstrumentConfig): import("@unworklet/core").CompiledProcessor<unknown>;
|
|
41
|
+
/** Diagnostic default: four voices, configurable at construction. */
|
|
42
|
+
export declare const instrument: import("@unworklet/core").CompiledProcessor<unknown>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { audioOutput, i32, defineProcessor, event, f32, forSample, instantiate, param, select, state } from '@unworklet/core';
|
|
2
|
+
import { envelope } from './envelope.js';
|
|
3
|
+
import { lfo, modulatePitch, modulateCutoff } from './lfo.js';
|
|
4
|
+
import { oscillator } from './oscillator.js';
|
|
5
|
+
import { filter } from './filter.js';
|
|
6
|
+
import { voicePolicy } from './voice-policy.js';
|
|
7
|
+
export { bassConfig, bassParameters, percussionConfig, percussionParameters, padConfig, padParameters } from './instrument-settings.js';
|
|
8
|
+
/** Diagnostic initial AudioParam values, not an approved preset or a preset format. */
|
|
9
|
+
export const diagnosticInstrumentParameters = {
|
|
10
|
+
gain: 0.15,
|
|
11
|
+
ampAttack: 0.005,
|
|
12
|
+
ampDecay: 0.08,
|
|
13
|
+
ampSustain: 0.7,
|
|
14
|
+
ampRelease: 0.15,
|
|
15
|
+
pitchAttack: 0,
|
|
16
|
+
pitchDecay: 0.1,
|
|
17
|
+
pitchSustain: 0,
|
|
18
|
+
pitchRelease: 0.1,
|
|
19
|
+
filterAttack: 0.01,
|
|
20
|
+
filterDecay: 0.15,
|
|
21
|
+
filterSustain: 0.2,
|
|
22
|
+
filterRelease: 0.15,
|
|
23
|
+
cutoff: 2500,
|
|
24
|
+
resonance: 0.707,
|
|
25
|
+
pitchEnvelopeDepth: 0,
|
|
26
|
+
filterEnvelopeDepth: 1,
|
|
27
|
+
lfoRate: 2,
|
|
28
|
+
lfoAmpDepth: 0,
|
|
29
|
+
lfoPitchDepth: 0,
|
|
30
|
+
lfoFilterDepth: 0,
|
|
31
|
+
bypass: 0,
|
|
32
|
+
};
|
|
33
|
+
/** MIDI input `midi`, stereo dual-mono output `main`, panic message `reset`.
|
|
34
|
+
* Capacities/mode/waveform/parts are construction choices; all musical controls
|
|
35
|
+
* use native AudioParams. See docs/instrument.md for timing and reset semantics.
|
|
36
|
+
*/
|
|
37
|
+
export function createInstrument(config) {
|
|
38
|
+
// Keep the tested compilation ceiling after the initial 32-voice graph
|
|
39
|
+
// exceeded the WASM function-size limit. This is not a real-time guarantee.
|
|
40
|
+
if ((config.capacity ?? (config.mode === 'mono' ? 1 : 4)) > 16) {
|
|
41
|
+
throw new RangeError('instrument capacity must be <= 16 (compiled graph limit)');
|
|
42
|
+
}
|
|
43
|
+
return defineProcessor(({ sampleRate }) => {
|
|
44
|
+
const policy = instantiate(voicePolicy, { ...config, capacity: config.capacity ?? (config.mode === 'mono' ? 1 : 4) }, { name: 'voices' });
|
|
45
|
+
const output = audioOutput({ channels: 2, name: 'main' });
|
|
46
|
+
const midi = event.midi({ from: 'main', name: 'midi' });
|
|
47
|
+
// A persisted marker paired with a transient marker distinguishes restore
|
|
48
|
+
// from a pristine instance even when MIDI already reactivated a voice.
|
|
49
|
+
const rendered = state.bool(false).named('rendered');
|
|
50
|
+
const live = state.bool(false).expose({ name: 'live', snapshot: 'transient' });
|
|
51
|
+
const panic = state.bool(false).expose({ name: 'panic', snapshot: 'transient' });
|
|
52
|
+
const voiceControls = state.buffer.bool({ size: 3 * policy.capacity }).expose({ name: 'voiceControls', snapshot: 'transient' });
|
|
53
|
+
const deferredRetrigger = state.buffer.bool({ size: policy.capacity }).expose({ name: 'deferredRetrigger', snapshot: 'transient' });
|
|
54
|
+
const completed = state.buffer.bool({ size: policy.capacity }).expose({ name: 'completed', snapshot: 'transient' });
|
|
55
|
+
const cleared = state.buffer.bool({ size: policy.capacity }).expose({ name: 'cleared', snapshot: 'transient' });
|
|
56
|
+
const tuning = state.buffer.f32({ size: 128 }).expose({ name: 'tuning', snapshot: 'transient' });
|
|
57
|
+
const modulation = instantiate(lfo, { sampleRate }, { name: 'lfo' });
|
|
58
|
+
// Materialize intermediate signals: 0.4.1 analysis recursively expands DAGs.
|
|
59
|
+
// Only cache musical exponents: below the state flush threshold (1e-30),
|
|
60
|
+
// their octave ratio already rounds to exactly 1. Do not cache final audio,
|
|
61
|
+
// where a finite sub-threshold result must survive downstream gain.
|
|
62
|
+
const depths = state.buffer.f32({ size: 2 * policy.capacity }).expose({ name: 'depths', snapshot: 'transient' });
|
|
63
|
+
const signals = state.buffer.f32({ size: 4 * policy.capacity + 1 }).expose({ name: 'signals', snapshot: 'transient' });
|
|
64
|
+
const gain = param.f32({ default: diagnosticInstrumentParameters.gain, min: 0, max: 1, automationRate: 'a-rate' }).named('gain');
|
|
65
|
+
const ampAttack = param.f32({ default: diagnosticInstrumentParameters.ampAttack, min: 0, max: 30, automationRate: 'a-rate' }).named('ampAttack');
|
|
66
|
+
const ampDecay = param.f32({ default: diagnosticInstrumentParameters.ampDecay, min: 0, max: 30, automationRate: 'a-rate' }).named('ampDecay');
|
|
67
|
+
const ampSustain = param.f32({ default: diagnosticInstrumentParameters.ampSustain, min: 0, max: 1, automationRate: 'a-rate' }).named('ampSustain');
|
|
68
|
+
const ampRelease = param.f32({ default: diagnosticInstrumentParameters.ampRelease, min: 0, max: 30, automationRate: 'a-rate' }).named('ampRelease');
|
|
69
|
+
const pitchAttack = param.f32({ default: diagnosticInstrumentParameters.pitchAttack, min: 0, max: 30, automationRate: 'a-rate' }).named('pitchAttack');
|
|
70
|
+
const pitchDecay = param.f32({ default: diagnosticInstrumentParameters.pitchDecay, min: 0, max: 30, automationRate: 'a-rate' }).named('pitchDecay');
|
|
71
|
+
const pitchSustain = param.f32({ default: diagnosticInstrumentParameters.pitchSustain, min: 0, max: 1, automationRate: 'a-rate' }).named('pitchSustain');
|
|
72
|
+
const pitchRelease = param.f32({ default: diagnosticInstrumentParameters.pitchRelease, min: 0, max: 30, automationRate: 'a-rate' }).named('pitchRelease');
|
|
73
|
+
const filterAttack = param.f32({ default: diagnosticInstrumentParameters.filterAttack, min: 0, max: 30, automationRate: 'a-rate' }).named('filterAttack');
|
|
74
|
+
const filterDecay = param.f32({ default: diagnosticInstrumentParameters.filterDecay, min: 0, max: 30, automationRate: 'a-rate' }).named('filterDecay');
|
|
75
|
+
const filterSustain = param.f32({ default: diagnosticInstrumentParameters.filterSustain, min: 0, max: 1, automationRate: 'a-rate' }).named('filterSustain');
|
|
76
|
+
const filterRelease = param.f32({ default: diagnosticInstrumentParameters.filterRelease, min: 0, max: 30, automationRate: 'a-rate' }).named('filterRelease');
|
|
77
|
+
const cutoff = param.f32({ default: diagnosticInstrumentParameters.cutoff, min: 20, max: 20000, automationRate: 'a-rate' }).named('cutoff');
|
|
78
|
+
const resonance = param.f32({ default: diagnosticInstrumentParameters.resonance, min: 0.5, max: 10, automationRate: 'a-rate' }).named('resonance');
|
|
79
|
+
const pitchEnvelopeDepth = param.f32({ default: diagnosticInstrumentParameters.pitchEnvelopeDepth, min: -24, max: 24, automationRate: 'a-rate' }).named('pitchEnvelopeDepth');
|
|
80
|
+
const filterEnvelopeDepth = param.f32({ default: diagnosticInstrumentParameters.filterEnvelopeDepth, min: -8, max: 8, automationRate: 'a-rate' }).named('filterEnvelopeDepth');
|
|
81
|
+
const lfoRate = param.f32({ default: diagnosticInstrumentParameters.lfoRate, min: 0, max: 20, automationRate: 'a-rate' }).named('lfoRate');
|
|
82
|
+
const lfoAmpDepth = param.f32({ default: diagnosticInstrumentParameters.lfoAmpDepth, min: 0, max: 1, automationRate: 'a-rate' }).named('lfoAmpDepth');
|
|
83
|
+
const lfoPitchDepth = param.f32({ default: diagnosticInstrumentParameters.lfoPitchDepth, min: -24, max: 24, automationRate: 'a-rate' }).named('lfoPitchDepth');
|
|
84
|
+
const lfoFilterDepth = param.f32({ default: diagnosticInstrumentParameters.lfoFilterDepth, min: -8, max: 8, automationRate: 'a-rate' }).named('lfoFilterDepth');
|
|
85
|
+
const bypass = param.f32({ default: diagnosticInstrumentParameters.bypass, min: 0, max: 1, automationRate: 'a-rate' }).named('bypass');
|
|
86
|
+
const parts = policy.voices.map((_, index) => ({
|
|
87
|
+
amp: instantiate(envelope, { sampleRate }, { name: `amp${index}` }),
|
|
88
|
+
pitch: instantiate(envelope, { sampleRate }, { name: `pitch${index}` }),
|
|
89
|
+
tone: instantiate(envelope, { sampleRate }, { name: `tone${index}` }),
|
|
90
|
+
oscillator: instantiate(config.oscillator ?? oscillator, { sampleRate, waveform: config.waveform ?? 'sine' }, { name: `oscillator${index}` }),
|
|
91
|
+
filter: instantiate(config.filter ?? filter, { sampleRate }, { name: `filter${index}` }),
|
|
92
|
+
}));
|
|
93
|
+
midi.onEvent('noteOn', e => policy.noteOn(e.note, e.channel, e.velocity));
|
|
94
|
+
midi.onEvent('noteOff', e => policy.noteOff(e.note, e.channel));
|
|
95
|
+
midi.onEvent('cc', e => {
|
|
96
|
+
// Capture ownership before CC120 frees/reassigns voices. A subsequent note
|
|
97
|
+
// in this quantum starts cold, even when it reuses the same physical slot.
|
|
98
|
+
policy.voices.forEach((voice, n) => cleared.write(n, cleared.read(n).or(e.controller.eq(120).and(voice.read().channel.eq(e.channel)))));
|
|
99
|
+
// These policy methods have no conditional argument. CC routing uses the
|
|
100
|
+
// policy's validated channel guard for unsupported controller messages.
|
|
101
|
+
policy.allNotesOff(select(e.controller.eq(123), e.channel, i32(-1)));
|
|
102
|
+
policy.allSoundOff(select(e.controller.eq(120), e.channel, i32(-1)));
|
|
103
|
+
});
|
|
104
|
+
event({ from: 'main', name: 'reset' }).onReceive(() => panic.write(true));
|
|
105
|
+
return { process() {
|
|
106
|
+
// Reset wins over MIDI delivered in the same block. Its DSP pulse is only
|
|
107
|
+
// the first sample; subsequent blocks may accept fresh notes normally.
|
|
108
|
+
policy.reset(panic.read());
|
|
109
|
+
for (let note = 0; note < 128; note++)
|
|
110
|
+
tuning.write(note, 440 * 2 ** ((note - 69) / 12));
|
|
111
|
+
forSample(i => {
|
|
112
|
+
const reset = panic.read();
|
|
113
|
+
signals.write(4 * policy.capacity, modulation.tick(lfoRate.at(i), reset, f32(0)));
|
|
114
|
+
let sum = f32(0);
|
|
115
|
+
policy.voices.forEach((voice, n) => {
|
|
116
|
+
const v = voice.read();
|
|
117
|
+
voiceControls.write(n * 3, reset.or(rendered.read().and(live.read().not())).or(v.active.not()).or(cleared.read(n)));
|
|
118
|
+
voiceControls.write(n * 3 + 1, v.gate.and(voiceControls.read(n * 3).not()));
|
|
119
|
+
voiceControls.write(n * 3 + 2, voice.takeRetrigger().or(deferredRetrigger.read(n)));
|
|
120
|
+
const clear = voiceControls.read(n * 3), gate = voiceControls.read(n * 3 + 1), trigger = voiceControls.read(n * 3 + 2);
|
|
121
|
+
// CC120 can suppress the note's first sample. Keep its reset pulse
|
|
122
|
+
// until the first audible sample, so oscillator phase starts at zero.
|
|
123
|
+
deferredRetrigger.write(n, clear.and(trigger).and(v.active).and(v.gate));
|
|
124
|
+
const p = parts[n];
|
|
125
|
+
const common = { gate, retrigger: trigger, reset: clear };
|
|
126
|
+
const amp = p.amp.tick({ ...common, attack: ampAttack.at(i), decay: ampDecay.at(i), sustain: ampSustain.at(i), release: ampRelease.at(i) });
|
|
127
|
+
const pitch = p.pitch.tick({ ...common, attack: pitchAttack.at(i), decay: pitchDecay.at(i), sustain: pitchSustain.at(i), release: pitchRelease.at(i) });
|
|
128
|
+
const tone = p.tone.tick({ ...common, attack: filterAttack.at(i), decay: filterDecay.at(i), sustain: filterSustain.at(i), release: filterRelease.at(i) });
|
|
129
|
+
signals.write(n * 4, pitch.level);
|
|
130
|
+
signals.write(n * 4 + 1, tone.level);
|
|
131
|
+
const wave = signals.read(4 * policy.capacity);
|
|
132
|
+
// Combine depths in musical units before the single destination clamp.
|
|
133
|
+
const semitones = signals.read(n * 4).mul(pitchEnvelopeDepth.at(i)).add(wave.mul(lfoPitchDepth.at(i)));
|
|
134
|
+
const octaves = signals.read(n * 4 + 1).mul(filterEnvelopeDepth.at(i)).add(wave.mul(lfoFilterDepth.at(i)));
|
|
135
|
+
depths.write(n * 2, semitones);
|
|
136
|
+
depths.write(n * 2 + 1, octaves);
|
|
137
|
+
signals.write(n * 4 + 2, modulatePitch(tuning.read(v.note.max(0)), f32(1), depths.read(n * 2), 0.45 * sampleRate));
|
|
138
|
+
signals.write(n * 4 + 3, modulateCutoff(cutoff.at(i), f32(1), depths.read(n * 2 + 1), Math.min(20000, 0.45 * sampleRate)));
|
|
139
|
+
// Preserve recoverable finite values across custom parts and amp gain.
|
|
140
|
+
// Native state writes flush tiny values, so these paths stay expressions.
|
|
141
|
+
const oscillation = p.oscillator.tick(signals.read(n * 4 + 2), clear.or(trigger));
|
|
142
|
+
const filtered = p.filter.tick(select(clear, f32(0), oscillation), signals.read(n * 4 + 3), resonance.at(i), clear.or(trigger));
|
|
143
|
+
// Unipolar tremolo: depth 0 is unity; depth 1 spans 0..1.
|
|
144
|
+
const tremolo = f32(1).sub(lfoAmpDepth.at(i).mul(f32(1).sub(wave)).mul(0.5));
|
|
145
|
+
const sample = select(v.active.and(clear.not()), filtered.mul(amp.level).mul(v.velocity).mul(tremolo), f32(0));
|
|
146
|
+
sum = sum.add(sample);
|
|
147
|
+
completed.write(n, amp.done);
|
|
148
|
+
cleared.write(n, 0);
|
|
149
|
+
});
|
|
150
|
+
const result = select(bypass.at(i).gte(0.5).or(reset), f32(0), sum.mul(gain.at(i)));
|
|
151
|
+
output.ch(0).at(i).write(result);
|
|
152
|
+
output.ch(1).at(i).write(result);
|
|
153
|
+
panic.write(false);
|
|
154
|
+
rendered.write(true);
|
|
155
|
+
live.write(true);
|
|
156
|
+
});
|
|
157
|
+
// MIDI is drained only before process(). Once the amp reaches zero it
|
|
158
|
+
// stays done for the rest of this block. Free completed allocations here,
|
|
159
|
+
// before the next MIDI dispatch, instead of doing O(voices²) rank updates
|
|
160
|
+
// on every sample. Audio termination remains on the exact envelope sample.
|
|
161
|
+
policy.voices.forEach((voice, n) => voice.releaseFinished(completed.read(n)));
|
|
162
|
+
} };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/** Diagnostic default: four voices, configurable at construction. */
|
|
166
|
+
export const instrument = createInstrument({ mode: 'poly' });
|
package/dist/lfo.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Node } from '@unworklet/core';
|
|
2
|
+
export interface LfoConfig {
|
|
3
|
+
sampleRate: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Free-running sine source; tick once per sample. Rate is clamped to [0,20] Hz.
|
|
7
|
+
* Output uses the current phase, then advances by rate/sampleRate. Rate zero
|
|
8
|
+
* holds phase; changing rate never resets phase. Reset emits the supplied phase
|
|
9
|
+
* on that sample and advances from it. Reset phase is in cycles, wrapped to [0,1).
|
|
10
|
+
* f64 phase prevents a small rate from stalling against a large f32 phase.
|
|
11
|
+
* Finite inputs required. No implicit note reset, depth, smoothing or routing.
|
|
12
|
+
*/
|
|
13
|
+
export declare const lfo: import("@unworklet/core").SubgraphDecl<[config: LfoConfig], {
|
|
14
|
+
tick(rate: Node<"f32">, reset: Node<"bool">, resetPhase: Node<"f32">): Node<"f32">;
|
|
15
|
+
}>;
|
|
16
|
+
/** Destination-unit depth, with final bounds enforced by the destination. */
|
|
17
|
+
export declare function modulatePitch(baseHz: Node<'f32'>, signal: Node<'f32'>, semitones: Node<'f32'>, maxHz: number): Node<"f32">;
|
|
18
|
+
export declare function modulateCutoff(baseHz: Node<'f32'>, signal: Node<'f32'>, octaves: Node<'f32'>, maxHz: number): Node<"f32">;
|
|
19
|
+
export declare function modulateDelay(baseSeconds: Node<'f32'>, signal: Node<'f32'>, depthSeconds: Node<'f32'>, minSeconds: number, maxSeconds: number): Node<"f32">;
|