@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,125 @@
|
|
|
1
|
+
import { defineSubgraph, f32, f64, i32, instantiate, select, state } from '@unworklet/core';
|
|
2
|
+
import { musicalClock } from './modulation.js';
|
|
3
|
+
function checkSampleRate(sampleRate) {
|
|
4
|
+
if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 192000) {
|
|
5
|
+
throw new RangeError('musical controls sampleRate must be an integer in [8000,192000]');
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
// Core 0.4.1 flushes tiny scalar-state writes. Scaling by this exact power of two
|
|
9
|
+
// preserves the entire finite f32 range in the bounded level/bend histories.
|
|
10
|
+
const SCALE = 2 ** 128;
|
|
11
|
+
/**
|
|
12
|
+
* Segment-relative curved ADSR. Tick exactly once per sample; finite controls.
|
|
13
|
+
* C(t,b)=t+b*t*(1-t), with exact endpoints. Segment settings latch on entry;
|
|
14
|
+
* decay latches at the preceding positive attack endpoint. Sustain follows its
|
|
15
|
+
* current input in the sustain stage. Retrigger/note-off start at the last
|
|
16
|
+
* emitted f32 level. Zero attack collapses into decay on the same sample.
|
|
17
|
+
* Reset > gate-off > retrigger. Reset consumes held gate. No smoothing/tail
|
|
18
|
+
* threshold; zero sustain remains active. Bend zero is linear, not a promise of
|
|
19
|
+
* byte identity with the legacy iterative envelope. See the entry contract.
|
|
20
|
+
*/
|
|
21
|
+
export const curvedAdsr = defineSubgraph((config) => {
|
|
22
|
+
checkSampleRate(config.sampleRate);
|
|
23
|
+
const stage = state.i32(0).named('stage'); // idle, attack, decay, sustain, release
|
|
24
|
+
const previousGate = state.bool(false).named('previousGate');
|
|
25
|
+
const total = state.i32(0).named('total');
|
|
26
|
+
const elapsed = state.i32(0).named('elapsed');
|
|
27
|
+
const start = state.f64(0).named('startScaled');
|
|
28
|
+
const target = state.f64(0).named('targetScaled');
|
|
29
|
+
const bend = state.f64(0).named('bendScaled');
|
|
30
|
+
const level = state.f64(0).named('levelScaled');
|
|
31
|
+
const frames = (seconds) => i32(f64(seconds.clamp(0, 30)).mul(config.sampleRate).add(0.5).floor());
|
|
32
|
+
return { tick(c) {
|
|
33
|
+
const oldStage = stage.read();
|
|
34
|
+
const on = c.gate.and(previousGate.read().not().or(c.retrigger));
|
|
35
|
+
const off = c.gate.not().and(previousGate.read()).and(oldStage.eq(0).not());
|
|
36
|
+
const a = frames(c.attack), d = frames(c.decay), r = frames(c.release);
|
|
37
|
+
const sustain = f64(c.sustain.clamp(0, 1)).mul(SCALE);
|
|
38
|
+
const attackBend = f64(c.attackBend.clamp(-1, 1)).mul(SCALE);
|
|
39
|
+
const decayBend = f64(c.decayBend.clamp(-1, 1)).mul(SCALE);
|
|
40
|
+
const releaseBend = f64(c.releaseBend.clamp(-1, 1)).mul(SCALE);
|
|
41
|
+
stage.write(select(off, i32(4), select(on, i32(1), oldStage)));
|
|
42
|
+
const active = stage.read(), attack = active.eq(1), release = active.eq(4);
|
|
43
|
+
const zeroAttack = attack.and(select(on, a, total.read()).eq(0));
|
|
44
|
+
const decay = active.eq(2).or(zeroAttack);
|
|
45
|
+
const moving = attack.or(decay).or(release);
|
|
46
|
+
total.write(select(release, select(off, r, total.read()), select(decay, select(zeroAttack, d, total.read()), select(on, a, total.read()))));
|
|
47
|
+
elapsed.write(select(on.or(off).or(zeroAttack), i32(0), elapsed.read()));
|
|
48
|
+
start.write(select(zeroAttack, f64(SCALE), select(on.or(off), level.read(), start.read())));
|
|
49
|
+
target.write(select(release, f64(0), select(decay, select(zeroAttack, sustain, target.read()), f64(SCALE))));
|
|
50
|
+
bend.write(select(release, select(off, releaseBend, bend.read()), select(decay, select(zeroAttack, decayBend, bend.read()), select(on, attackBend, bend.read()))));
|
|
51
|
+
elapsed.write(select(moving, elapsed.read().add(1).min(total.read()), elapsed.read()));
|
|
52
|
+
const t = f64(elapsed.read()).div(f64(total.read().max(1)));
|
|
53
|
+
const curved = t.add(bend.read().div(SCALE).mul(t).mul(f64(1).sub(t)));
|
|
54
|
+
const last = elapsed.read().gte(total.read()).or(release.and(start.read().eq(0)));
|
|
55
|
+
const advanced = select(last, target.read(), start.read().add(target.read().sub(start.read()).mul(curved)));
|
|
56
|
+
const emitted = f32(select(c.reset, f64(0), select(moving, advanced, select(active.eq(3), sustain, f64(0)))).div(SCALE)).clamp(0, 1);
|
|
57
|
+
// Store the actual f32 output: interruption starts at the observable level,
|
|
58
|
+
// not a hidden higher-precision value or a post-warped absolute amplitude.
|
|
59
|
+
level.write(f64(emitted).mul(SCALE));
|
|
60
|
+
const nextStage = select(c.reset, i32(0), select(moving.and(last), select(release, i32(0), select(decay, i32(3), i32(2))), select(decay, i32(2), active)));
|
|
61
|
+
const prepareDecay = attack.and(decay.not()).and(last);
|
|
62
|
+
total.write(select(c.reset, i32(0), select(prepareDecay, d, total.read())));
|
|
63
|
+
elapsed.write(select(c.reset.or(prepareDecay), i32(0), elapsed.read()));
|
|
64
|
+
start.write(select(c.reset, f64(0), select(prepareDecay, f64(SCALE), start.read())));
|
|
65
|
+
target.write(select(c.reset, f64(0), select(prepareDecay, sustain, target.read())));
|
|
66
|
+
bend.write(select(c.reset, f64(0), select(prepareDecay, decayBend, bend.read())));
|
|
67
|
+
stage.write(nextStage);
|
|
68
|
+
previousGate.write(c.gate);
|
|
69
|
+
return { level: f32(level.read().div(SCALE)), done: nextStage.eq(0) };
|
|
70
|
+
} };
|
|
71
|
+
});
|
|
72
|
+
const PHASE_MAX = 1 - 2 ** -24;
|
|
73
|
+
function sineAtPhase(phase) {
|
|
74
|
+
const centered = phase.sub(select(phase.gt(.5), f64(1), f64(0)));
|
|
75
|
+
const folded = select(centered.gt(.25), f64(.5).sub(centered), select(centered.lt(-.25), f64(-.5).sub(centered), centered));
|
|
76
|
+
const x = folded.mul(2 * Math.PI), x2 = x.mul(x);
|
|
77
|
+
// Same degree-13 reflected sine as the legacy LFO. At this canonical public
|
|
78
|
+
// f32 phase, |Taylor remainder| <= (pi/2)^15/15! < 7e-10 before output rounding.
|
|
79
|
+
const polynomial = f64(1 / 6227020800).mul(x2).sub(1 / 39916800)
|
|
80
|
+
.mul(x2).add(1 / 362880).mul(x2).sub(1 / 5040)
|
|
81
|
+
.mul(x2).add(1 / 120).mul(x2).sub(1 / 6).mul(x2).add(1);
|
|
82
|
+
return x.mul(polynomial);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Fixed-waveform LFO composed from musicalClock. Tick once per sample with finite
|
|
86
|
+
* inputs. Output precedes integration. Held reset pins base zero; release emits
|
|
87
|
+
* zero base before resuming. Hold/rate zero preserve base phase. Tempo follows
|
|
88
|
+
* BPM/(60*beatsPerCycle) exactly within the clock's numerical contract, including
|
|
89
|
+
* up to 1066 2/3 Hz. Public f32 clock-phase quantization is intentional, not the
|
|
90
|
+
* legacy sine LFO's internal-f64 phase guarantee. Waveforms are non-bandlimited;
|
|
91
|
+
* phase/offset/reset changes can jump. No smoothing/depth/routing/transport.
|
|
92
|
+
*/
|
|
93
|
+
export const musicalLfo = defineSubgraph((config) => {
|
|
94
|
+
checkSampleRate(config.sampleRate);
|
|
95
|
+
if (config.mode !== 'free' && config.mode !== 'tempo')
|
|
96
|
+
throw new RangeError('musicalLfo mode must be free or tempo');
|
|
97
|
+
if (!['sine', 'triangle', 'saw', 'square'].includes(config.waveform))
|
|
98
|
+
throw new RangeError('unsupported musicalLfo waveform');
|
|
99
|
+
const beats = config.beatsPerCycle ?? 1;
|
|
100
|
+
if (!Number.isFinite(beats) || beats < 1 / 64 || beats > 64 || !Number.isInteger(Math.log2(beats))) {
|
|
101
|
+
throw new RangeError('beatsPerCycle must be a power of two in [1/64,64]');
|
|
102
|
+
}
|
|
103
|
+
if (config.mode === 'free' && beats !== 1)
|
|
104
|
+
throw new RangeError('beatsPerCycle applies only to tempo mode');
|
|
105
|
+
const clock = instantiate(musicalClock, {
|
|
106
|
+
sampleRate: config.sampleRate, mode: config.mode, steps: 1, stepsPerBeat: config.mode === 'tempo' ? 1 / beats : 1,
|
|
107
|
+
}, { name: 'clock' });
|
|
108
|
+
return { tick(c) {
|
|
109
|
+
const base = clock.tick({ rate: select(c.hold, f32(0), c.rate.clamp(0, config.mode === 'free' ? 20 : 1000)),
|
|
110
|
+
reset: c.reset, seek: c.seek, position: c.position });
|
|
111
|
+
const offset = f64(c.phaseOffset.clamp(-1048576, 1048576));
|
|
112
|
+
// Remove the integer part toward zero, preserving negative tiny fractions.
|
|
113
|
+
// Add only the signed fraction so large integral offsets cannot erase a
|
|
114
|
+
// tiny base phase. Wrap once afterward and cap the rounded upper endpoint.
|
|
115
|
+
const fraction = offset.sub(select(offset.lt(0), offset.neg().floor().neg(), offset.floor()));
|
|
116
|
+
const shifted = f64(base.phase).add(fraction);
|
|
117
|
+
const phase = f32(shifted.sub(shifted.floor())).clamp(0, PHASE_MAX);
|
|
118
|
+
const p = f64(phase);
|
|
119
|
+
const value = config.waveform === 'sine' ? sineAtPhase(p)
|
|
120
|
+
: config.waveform === 'triangle' ? select(p.lt(.25), p.mul(4), select(p.lt(.75), f64(2).sub(p.mul(4)), p.mul(4).sub(4)))
|
|
121
|
+
: config.waveform === 'saw' ? p.mul(2).sub(1)
|
|
122
|
+
: select(p.lt(.5), f64(1), f64(-1));
|
|
123
|
+
return { value: f32(value).clamp(-1, 1), phase };
|
|
124
|
+
} };
|
|
125
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { musicalMaterialsStatus, glassDyadParameters, glassDyadBrightParameters, glassDyadConstruction, makeGlassDyadTable, fmModalHitParameters, fmModalHitBellParameters, fmModalHitModes, grainCloudParameters, grainCloudReverseParameters, grainCloudConstruction, makeGrainCloudSample, shapedEchoParameters, shapedEchoDarkParameters, } from './musical-materials.js';
|
|
2
|
+
/** CANDIDATE fixed two-voice instrument. Independent native gate/frequency params,
|
|
3
|
+
* shared resident morph table and ordinary native snapshot state; no MIDI allocator.
|
|
4
|
+
* Send `load` with makeGlassDyadTable() before gate-on. Output `main` is stereo.
|
|
5
|
+
*/
|
|
6
|
+
export declare const glassDyad: import("@unworklet/core").CompiledProcessor<unknown>;
|
|
7
|
+
/** CANDIDATE fixed-pitch A3 struck voice. Gate rise excites one normalized modal
|
|
8
|
+
* impulse and restarts FM/amp. Each new hit clears the old modal tail, keeping
|
|
9
|
+
* its impulse bound; gate-off releases FM while the fixed modal tail continues.
|
|
10
|
+
*/
|
|
11
|
+
export declare const fmModalHit: import("@unworklet/core").CompiledProcessor<unknown>;
|
|
12
|
+
/** CANDIDATE two-grain resident texture. `load` accepts makeGrainCloudSample().
|
|
13
|
+
* Gate-off stops new grains and lets existing windows finish. Parameters latch
|
|
14
|
+
* at each grain onset as specified by granularSource; no streaming or time stretch.
|
|
15
|
+
*/
|
|
16
|
+
export declare const grainCloud: import("@unworklet/core").CompiledProcessor<unknown>;
|
|
17
|
+
/** CANDIDATE mono-in/stereo-out chain: soft first-order ADAA -> Q=.5 low-pass
|
|
18
|
+
* -> asymmetric feed into stereo ping-pong. `main` input is finite mono <=1.
|
|
19
|
+
* Dry/wet is intentional delay coloration; no external parallel-path alignment.
|
|
20
|
+
*/
|
|
21
|
+
export declare const shapedEcho: import("@unworklet/core").CompiledProcessor<unknown>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { audioInput, audioOutput, bool, CAPACITY_16, defineProcessor, event, f32, forSample, instantiate, param, select, state, } from '@unworklet/core';
|
|
2
|
+
import { envelope } from './envelope.js';
|
|
3
|
+
import { residentSample, granularSource } from './sample.js';
|
|
4
|
+
import { wavetableSource } from './wavetable.js';
|
|
5
|
+
import { frequencyModulation } from './source.js';
|
|
6
|
+
import { modalResonator } from './resonator.js';
|
|
7
|
+
import { drive } from './drive.js';
|
|
8
|
+
import { stateVariableFilter } from './state-variable-filter.js';
|
|
9
|
+
import { pingPongDelay } from './stereo-delay.js';
|
|
10
|
+
import { glassDyadParameters as glass, fmModalHitParameters as hit, grainCloudParameters as grain, shapedEchoParameters as echo, fmModalHitModes, } from './musical-materials.js';
|
|
11
|
+
export { musicalMaterialsStatus, glassDyadParameters, glassDyadBrightParameters, glassDyadConstruction, makeGlassDyadTable, fmModalHitParameters, fmModalHitBellParameters, fmModalHitModes, grainCloudParameters, grainCloudReverseParameters, grainCloudConstruction, makeGrainCloudSample, shapedEchoParameters, shapedEchoDarkParameters, } from './musical-materials.js';
|
|
12
|
+
/** CANDIDATE fixed two-voice instrument. Independent native gate/frequency params,
|
|
13
|
+
* shared resident morph table and ordinary native snapshot state; no MIDI allocator.
|
|
14
|
+
* Send `load` with makeGlassDyadTable() before gate-on. Output `main` is stereo.
|
|
15
|
+
*/
|
|
16
|
+
export const glassDyad = defineProcessor(({ sampleRate }) => {
|
|
17
|
+
const asset = instantiate(residentSample, { capacity: 384, sourceSampleRate: 48000 }, { name: 'asset' });
|
|
18
|
+
event({ from: 'main', name: 'load', capacity: CAPACITY_16, payloadCapacity: 1536 })
|
|
19
|
+
.onReceive(({ data }) => asset.load(data));
|
|
20
|
+
const gateA = param.f32({ default: glass.gateA, min: 0, max: 1, automationRate: 'a-rate' }).named('gateA');
|
|
21
|
+
const gateB = param.f32({ default: glass.gateB, min: 0, max: 1, automationRate: 'a-rate' }).named('gateB');
|
|
22
|
+
const frequencyA = param.f32({ default: glass.frequencyA, min: 55, max: 1200, automationRate: 'a-rate' }).named('frequencyA');
|
|
23
|
+
const frequencyB = param.f32({ default: glass.frequencyB, min: 55, max: 1200, automationRate: 'a-rate' }).named('frequencyB');
|
|
24
|
+
const frame = param.f32({ default: glass.frame, min: 0, max: 2, automationRate: 'a-rate' }).named('frame');
|
|
25
|
+
const gain = param.f32({ default: glass.gain, min: 0, max: .3, automationRate: 'a-rate' }).named('gain');
|
|
26
|
+
const reset = param.f32({ default: 0, min: 0, max: 1, automationRate: 'a-rate' }).named('reset');
|
|
27
|
+
const voices = [0, 1].map(n => ({
|
|
28
|
+
wave: instantiate(wavetableSource, { sampleRate, sample: asset, frameLength: 128, frameCount: 3 }, { name: `wave${n}` }),
|
|
29
|
+
amp: instantiate(envelope, { sampleRate }, { name: `amp${n}` }),
|
|
30
|
+
gate: state.bool(false).named(`previousGate${n}`),
|
|
31
|
+
}));
|
|
32
|
+
const out = audioOutput({ channels: 2, name: 'main' });
|
|
33
|
+
return { process() {
|
|
34
|
+
forSample(i => {
|
|
35
|
+
const clear = reset.at(i).gte(.5);
|
|
36
|
+
const values = voices.map((v, n) => {
|
|
37
|
+
const gate = (n === 0 ? gateA : gateB).at(i).gte(.5), trigger = gate.and(v.gate.read().not());
|
|
38
|
+
const amp = v.amp.tick({ gate, retrigger: bool(false), reset: clear,
|
|
39
|
+
attack: f32(.012), decay: f32(.2), sustain: f32(.65), release: f32(.28) });
|
|
40
|
+
const wave = v.wave.tick({ frequencyHz: (n === 0 ? frequencyA : frequencyB).at(i), frame: frame.at(i), reset: clear.or(trigger) });
|
|
41
|
+
v.gate.write(gate);
|
|
42
|
+
return select(clear, f32(0), wave.output.mul(amp.level));
|
|
43
|
+
});
|
|
44
|
+
out.ch(0).at(i).write(values[0].mul(.75).add(values[1].mul(.25)).mul(gain.at(i)));
|
|
45
|
+
out.ch(1).at(i).write(values[0].mul(.25).add(values[1].mul(.75)).mul(gain.at(i)));
|
|
46
|
+
});
|
|
47
|
+
} };
|
|
48
|
+
});
|
|
49
|
+
/** CANDIDATE fixed-pitch A3 struck voice. Gate rise excites one normalized modal
|
|
50
|
+
* impulse and restarts FM/amp. Each new hit clears the old modal tail, keeping
|
|
51
|
+
* its impulse bound; gate-off releases FM while the fixed modal tail continues.
|
|
52
|
+
*/
|
|
53
|
+
export const fmModalHit = defineProcessor(({ sampleRate }) => {
|
|
54
|
+
const gate = param.f32({ default: hit.gate, min: 0, max: 1, automationRate: 'a-rate' }).named('gate');
|
|
55
|
+
const strength = param.f32({ default: hit.strength, min: 0, max: 1, automationRate: 'a-rate' }).named('strength');
|
|
56
|
+
const ratio = param.f32({ default: hit.ratio, min: .5, max: 4, automationRate: 'a-rate' }).named('ratio');
|
|
57
|
+
const deviation = param.f32({ default: hit.deviationHz, min: 0, max: 180, automationRate: 'a-rate' }).named('deviationHz');
|
|
58
|
+
const mix = param.f32({ default: hit.modalMix, min: 0, max: 1, automationRate: 'a-rate' }).named('modalMix');
|
|
59
|
+
const gain = param.f32({ default: hit.gain, min: 0, max: .3, automationRate: 'a-rate' }).named('gain');
|
|
60
|
+
const reset = param.f32({ default: 0, min: 0, max: 1, automationRate: 'a-rate' }).named('reset');
|
|
61
|
+
const previousGate = state.bool(false).named('previousGate');
|
|
62
|
+
const velocity = state.f32(0).named('strength');
|
|
63
|
+
const fm = instantiate(frequencyModulation, { sampleRate }, { name: 'fm' });
|
|
64
|
+
const modes = instantiate(modalResonator, { sampleRate, modes: fmModalHitModes }, { name: 'modes' });
|
|
65
|
+
const amp = instantiate(envelope, { sampleRate }, { name: 'amp' });
|
|
66
|
+
const out = audioOutput({ channels: 2, name: 'main' });
|
|
67
|
+
return { process() {
|
|
68
|
+
forSample(i => {
|
|
69
|
+
const on = gate.at(i).gte(.5), clear = reset.at(i).gte(.5);
|
|
70
|
+
const strike = on.and(previousGate.read().not()).and(clear.not());
|
|
71
|
+
velocity.write(select(clear, f32(0), select(strike, strength.at(i), velocity.read())));
|
|
72
|
+
const a = amp.tick({ gate: on, retrigger: strike, reset: clear,
|
|
73
|
+
attack: f32(.001), decay: f32(.18), sustain: f32(0), release: f32(.04) });
|
|
74
|
+
const tone = fm.tick({ carrierHz: f32(220), modulatorHz: ratio.at(i).mul(220),
|
|
75
|
+
deviationHz: deviation.at(i).mul(a.level), feedbackHz: f32(0), reset: clear.or(strike) });
|
|
76
|
+
const ring = modes.tick(select(strike, velocity.read(), f32(0)), clear.or(strike));
|
|
77
|
+
const m = mix.at(i);
|
|
78
|
+
const value = select(clear, f32(0), tone.mul(a.level).mul(velocity.read()).mul(f32(1).sub(m)).add(ring.mul(m)).mul(gain.at(i)));
|
|
79
|
+
out.ch(0).at(i).write(value);
|
|
80
|
+
out.ch(1).at(i).write(value);
|
|
81
|
+
previousGate.write(on);
|
|
82
|
+
});
|
|
83
|
+
} };
|
|
84
|
+
});
|
|
85
|
+
/** CANDIDATE two-grain resident texture. `load` accepts makeGrainCloudSample().
|
|
86
|
+
* Gate-off stops new grains and lets existing windows finish. Parameters latch
|
|
87
|
+
* at each grain onset as specified by granularSource; no streaming or time stretch.
|
|
88
|
+
*/
|
|
89
|
+
export const grainCloud = defineProcessor(({ sampleRate }) => {
|
|
90
|
+
const asset = instantiate(residentSample, { capacity: 4096, sourceSampleRate: 48000 }, { name: 'asset' });
|
|
91
|
+
event({ from: 'main', name: 'load', capacity: CAPACITY_16, payloadCapacity: 16384 })
|
|
92
|
+
.onReceive(({ data }) => asset.load(data));
|
|
93
|
+
const source = instantiate(granularSource, { sampleRate, sample: asset, maxGrains: 2, seed: 1741, loop: true }, { name: 'grains' });
|
|
94
|
+
const gate = param.f32({ default: 0, min: 0, max: 1, automationRate: 'a-rate' }).named('gate');
|
|
95
|
+
const position = param.f32({ default: grain.positionFrames, min: 0, max: 4095, automationRate: 'a-rate' }).named('positionFrames');
|
|
96
|
+
const jitter = param.f32({ default: grain.jitterFrames, min: 0, max: 4095, automationRate: 'a-rate' }).named('jitterFrames');
|
|
97
|
+
const rate = param.f32({ default: grain.rate, min: -2, max: 2, automationRate: 'a-rate' }).named('rate');
|
|
98
|
+
const duration = param.f32({ default: grain.durationSeconds, min: .02, max: .15, automationRate: 'a-rate' }).named('durationSeconds');
|
|
99
|
+
const density = param.f32({ default: grain.densityHz, min: 0, max: 20, automationRate: 'a-rate' }).named('densityHz');
|
|
100
|
+
const gain = param.f32({ default: grain.gain, min: 0, max: .4, automationRate: 'a-rate' }).named('gain');
|
|
101
|
+
const reset = param.f32({ default: 0, min: 0, max: 1, automationRate: 'a-rate' }).named('reset');
|
|
102
|
+
const out = audioOutput({ channels: 2, name: 'main' });
|
|
103
|
+
return { process() {
|
|
104
|
+
forSample(i => {
|
|
105
|
+
const result = source.tick({ gate: gate.at(i).gte(.5), reset: reset.at(i).gte(.5),
|
|
106
|
+
positionFrames: position.at(i), jitterFrames: jitter.at(i), rate: rate.at(i),
|
|
107
|
+
durationSeconds: duration.at(i), densityHz: density.at(i) });
|
|
108
|
+
const value = result.output.mul(gain.at(i));
|
|
109
|
+
out.ch(0).at(i).write(value);
|
|
110
|
+
out.ch(1).at(i).write(value);
|
|
111
|
+
});
|
|
112
|
+
} };
|
|
113
|
+
});
|
|
114
|
+
/** CANDIDATE mono-in/stereo-out chain: soft first-order ADAA -> Q=.5 low-pass
|
|
115
|
+
* -> asymmetric feed into stereo ping-pong. `main` input is finite mono <=1.
|
|
116
|
+
* Dry/wet is intentional delay coloration; no external parallel-path alignment.
|
|
117
|
+
*/
|
|
118
|
+
export const shapedEcho = defineProcessor(({ sampleRate }) => {
|
|
119
|
+
const input = audioInput({ channels: 1, name: 'main' });
|
|
120
|
+
const out = audioOutput({ channels: 2, name: 'main' });
|
|
121
|
+
const shaper = instantiate(drive, { sampleRate, curve: 'soft', quality: 'adaa' }, { name: 'drive' });
|
|
122
|
+
const filter = instantiate(stateVariableFilter, { sampleRate }, { name: 'filter' });
|
|
123
|
+
const delay = instantiate(pingPongDelay, { sampleRate, maxDelaySeconds: .25 }, { name: 'echo' });
|
|
124
|
+
const driveGain = param.f32({ default: echo.drive, min: 0, max: 8, automationRate: 'a-rate' }).named('drive');
|
|
125
|
+
const cutoff = param.f32({ default: echo.cutoffHz, min: 100, max: 4000, automationRate: 'a-rate' }).named('cutoffHz');
|
|
126
|
+
const time = param.f32({ default: echo.delaySeconds, min: .04, max: .25, automationRate: 'a-rate' }).named('delaySeconds');
|
|
127
|
+
const feedback = param.f32({ default: echo.feedback, min: 0, max: .4, automationRate: 'a-rate' }).named('feedback');
|
|
128
|
+
const mix = param.f32({ default: echo.mix, min: 0, max: .6, automationRate: 'a-rate' }).named('mix');
|
|
129
|
+
const gain = param.f32({ default: echo.gain, min: 0, max: .25, automationRate: 'a-rate' }).named('gain');
|
|
130
|
+
const reset = param.f32({ default: 0, min: 0, max: 1, automationRate: 'a-rate' }).named('reset');
|
|
131
|
+
return { process() {
|
|
132
|
+
forSample(i => {
|
|
133
|
+
const clear = reset.at(i).gte(.5);
|
|
134
|
+
const x = shaper.tick(select(clear, f32(0), input.ch(0).at(i)), driveGain.at(i), f32(1), clear);
|
|
135
|
+
const tone = filter.tick(x, cutoff.at(i), f32(.5), clear).lowpass;
|
|
136
|
+
const wet = delay.tick(tone, tone.mul(.5), { timeSeconds: time.at(i), feedback: feedback.at(i),
|
|
137
|
+
mix: mix.at(i), bypass: bool(false), reset: clear });
|
|
138
|
+
out.ch(0).at(i).write(select(clear, f32(0), wet.left.mul(gain.at(i))));
|
|
139
|
+
out.ch(1).at(i).write(select(clear, f32(0), wet.right.mul(gain.at(i))));
|
|
140
|
+
});
|
|
141
|
+
} };
|
|
142
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** Original synthetic materials and ordinary native AudioParam values.
|
|
2
|
+
* These are CANDIDATE examples, not a preset format or listening-approved audio.
|
|
3
|
+
* Asset creation runs on the host before the native `load` message is sent.
|
|
4
|
+
*/
|
|
5
|
+
export declare const musicalMaterialsStatus: "CANDIDATE";
|
|
6
|
+
export declare const glassDyadParameters: {
|
|
7
|
+
readonly gateA: 0;
|
|
8
|
+
readonly gateB: 0;
|
|
9
|
+
readonly frequencyA: 220;
|
|
10
|
+
readonly frequencyB: 330;
|
|
11
|
+
readonly frame: 0.25;
|
|
12
|
+
readonly gain: 0.2;
|
|
13
|
+
readonly reset: 0;
|
|
14
|
+
};
|
|
15
|
+
export declare const glassDyadBrightParameters: {
|
|
16
|
+
readonly frame: 1.75;
|
|
17
|
+
readonly gateA: 0;
|
|
18
|
+
readonly gateB: 0;
|
|
19
|
+
readonly frequencyA: 220;
|
|
20
|
+
readonly frequencyB: 330;
|
|
21
|
+
readonly gain: 0.2;
|
|
22
|
+
readonly reset: 0;
|
|
23
|
+
};
|
|
24
|
+
export declare const glassDyadConstruction: {
|
|
25
|
+
readonly voices: 2;
|
|
26
|
+
readonly frameLength: 128;
|
|
27
|
+
readonly frameCount: 3;
|
|
28
|
+
readonly capacity: 384;
|
|
29
|
+
};
|
|
30
|
+
/** Three complete 128-sample cycles, no duplicate endpoint; fixed weights, no normalization. */
|
|
31
|
+
export declare function makeGlassDyadTable(): Float32Array;
|
|
32
|
+
export declare const fmModalHitParameters: {
|
|
33
|
+
readonly gate: 0;
|
|
34
|
+
readonly strength: 0.8;
|
|
35
|
+
readonly ratio: 1.5;
|
|
36
|
+
readonly deviationHz: 100;
|
|
37
|
+
readonly modalMix: 0.35;
|
|
38
|
+
readonly gain: 0.22;
|
|
39
|
+
readonly reset: 0;
|
|
40
|
+
};
|
|
41
|
+
export declare const fmModalHitBellParameters: {
|
|
42
|
+
readonly ratio: 2.75;
|
|
43
|
+
readonly deviationHz: 170;
|
|
44
|
+
readonly modalMix: 0.7;
|
|
45
|
+
readonly gate: 0;
|
|
46
|
+
readonly strength: 0.8;
|
|
47
|
+
readonly gain: 0.22;
|
|
48
|
+
readonly reset: 0;
|
|
49
|
+
};
|
|
50
|
+
export declare const fmModalHitModes: readonly [{
|
|
51
|
+
readonly frequencyHz: 220;
|
|
52
|
+
readonly decaySeconds: 0.42;
|
|
53
|
+
readonly gain: 1;
|
|
54
|
+
}, {
|
|
55
|
+
readonly frequencyHz: 351;
|
|
56
|
+
readonly decaySeconds: 0.24;
|
|
57
|
+
readonly gain: 0.6;
|
|
58
|
+
}, {
|
|
59
|
+
readonly frequencyHz: 563;
|
|
60
|
+
readonly decaySeconds: 0.15;
|
|
61
|
+
readonly gain: 0.35;
|
|
62
|
+
}];
|
|
63
|
+
export declare const grainCloudParameters: {
|
|
64
|
+
readonly gate: 0;
|
|
65
|
+
readonly positionFrames: 512;
|
|
66
|
+
readonly jitterFrames: 240;
|
|
67
|
+
readonly rate: 1;
|
|
68
|
+
readonly durationSeconds: 0.09;
|
|
69
|
+
readonly densityHz: 16;
|
|
70
|
+
readonly gain: 0.28;
|
|
71
|
+
readonly reset: 0;
|
|
72
|
+
};
|
|
73
|
+
export declare const grainCloudReverseParameters: {
|
|
74
|
+
readonly positionFrames: 2800;
|
|
75
|
+
readonly rate: -0.75;
|
|
76
|
+
readonly durationSeconds: 0.12;
|
|
77
|
+
readonly densityHz: 12;
|
|
78
|
+
readonly gate: 0;
|
|
79
|
+
readonly jitterFrames: 240;
|
|
80
|
+
readonly gain: 0.28;
|
|
81
|
+
readonly reset: 0;
|
|
82
|
+
};
|
|
83
|
+
export declare const grainCloudConstruction: {
|
|
84
|
+
readonly capacity: 4096;
|
|
85
|
+
readonly sourceSampleRate: 48000;
|
|
86
|
+
readonly maxGrains: 2;
|
|
87
|
+
readonly seed: 1741;
|
|
88
|
+
};
|
|
89
|
+
/** Original periodic tone cluster with a smooth fixed amplitude contour.
|
|
90
|
+
* Integer cycle counts make the short resident loop continuous. No recorded audio.
|
|
91
|
+
*/
|
|
92
|
+
export declare function makeGrainCloudSample(): Float32Array;
|
|
93
|
+
export declare const shapedEchoParameters: {
|
|
94
|
+
readonly drive: 2.4;
|
|
95
|
+
readonly cutoffHz: 1800;
|
|
96
|
+
readonly delaySeconds: 0.125;
|
|
97
|
+
readonly feedback: 0.25;
|
|
98
|
+
readonly mix: 0.3;
|
|
99
|
+
readonly gain: 0.2;
|
|
100
|
+
readonly reset: 0;
|
|
101
|
+
};
|
|
102
|
+
export declare const shapedEchoDarkParameters: {
|
|
103
|
+
readonly drive: 4.5;
|
|
104
|
+
readonly cutoffHz: 650;
|
|
105
|
+
readonly delaySeconds: 0.1875;
|
|
106
|
+
readonly mix: 0.45;
|
|
107
|
+
readonly feedback: 0.25;
|
|
108
|
+
readonly gain: 0.2;
|
|
109
|
+
readonly reset: 0;
|
|
110
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Original synthetic materials and ordinary native AudioParam values.
|
|
2
|
+
* These are CANDIDATE examples, not a preset format or listening-approved audio.
|
|
3
|
+
* Asset creation runs on the host before the native `load` message is sent.
|
|
4
|
+
*/
|
|
5
|
+
export const musicalMaterialsStatus = 'CANDIDATE';
|
|
6
|
+
export const glassDyadParameters = {
|
|
7
|
+
gateA: 0, gateB: 0, frequencyA: 220, frequencyB: 330,
|
|
8
|
+
frame: .25, gain: .2, reset: 0,
|
|
9
|
+
};
|
|
10
|
+
export const glassDyadBrightParameters = { ...glassDyadParameters, frame: 1.75 };
|
|
11
|
+
export const glassDyadConstruction = { voices: 2, frameLength: 128, frameCount: 3, capacity: 384 };
|
|
12
|
+
/** Three complete 128-sample cycles, no duplicate endpoint; fixed weights, no normalization. */
|
|
13
|
+
export function makeGlassDyadTable() {
|
|
14
|
+
return Float32Array.from({ length: 384 }, (_, n) => {
|
|
15
|
+
const phase = 2 * Math.PI * (n % 128) / 128;
|
|
16
|
+
const frame = Math.floor(n / 128);
|
|
17
|
+
return frame === 0 ? .8 * Math.sin(phase)
|
|
18
|
+
: frame === 1 ? .6 * Math.sin(phase) + .2 * Math.sin(2 * phase)
|
|
19
|
+
: .4 * Math.sin(phase) + .25 * Math.sin(2 * phase) + .15 * Math.sin(3 * phase);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export const fmModalHitParameters = {
|
|
23
|
+
gate: 0, strength: .8, ratio: 1.5, deviationHz: 100, modalMix: .35, gain: .22, reset: 0,
|
|
24
|
+
};
|
|
25
|
+
export const fmModalHitBellParameters = { ...fmModalHitParameters, ratio: 2.75, deviationHz: 170, modalMix: .7 };
|
|
26
|
+
export const fmModalHitModes = [
|
|
27
|
+
{ frequencyHz: 220, decaySeconds: .42, gain: 1 },
|
|
28
|
+
{ frequencyHz: 351, decaySeconds: .24, gain: .6 },
|
|
29
|
+
{ frequencyHz: 563, decaySeconds: .15, gain: .35 },
|
|
30
|
+
];
|
|
31
|
+
export const grainCloudParameters = {
|
|
32
|
+
gate: 0, positionFrames: 512, jitterFrames: 240, rate: 1,
|
|
33
|
+
durationSeconds: .09, densityHz: 16, gain: .28, reset: 0,
|
|
34
|
+
};
|
|
35
|
+
export const grainCloudReverseParameters = {
|
|
36
|
+
...grainCloudParameters, positionFrames: 2800, rate: -.75,
|
|
37
|
+
durationSeconds: .12, densityHz: 12,
|
|
38
|
+
};
|
|
39
|
+
export const grainCloudConstruction = { capacity: 4096, sourceSampleRate: 48000, maxGrains: 2, seed: 1741 };
|
|
40
|
+
/** Original periodic tone cluster with a smooth fixed amplitude contour.
|
|
41
|
+
* Integer cycle counts make the short resident loop continuous. No recorded audio.
|
|
42
|
+
*/
|
|
43
|
+
export function makeGrainCloudSample() {
|
|
44
|
+
return Float32Array.from({ length: 4096 }, (_, n) => {
|
|
45
|
+
const phase = 2 * Math.PI * n / 4096;
|
|
46
|
+
const contour = .55 - .45 * Math.cos(phase);
|
|
47
|
+
return contour * (.32 * Math.sin(19 * phase) + .17 * Math.sin(31 * phase) + .08 * Math.sin(47 * phase));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export const shapedEchoParameters = {
|
|
51
|
+
drive: 2.4, cutoffHz: 1800, delaySeconds: .125, feedback: .25,
|
|
52
|
+
mix: .3, gain: .2, reset: 0,
|
|
53
|
+
};
|
|
54
|
+
export const shapedEchoDarkParameters = {
|
|
55
|
+
...shapedEchoParameters, drive: 4.5, cutoffHz: 650, delaySeconds: .1875, mix: .45,
|
|
56
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Node } from '@unworklet/core';
|
|
2
|
+
export interface OscillatorConfig {
|
|
3
|
+
sampleRate: number;
|
|
4
|
+
waveform: 'sine' | 'saw';
|
|
5
|
+
}
|
|
6
|
+
/** Phase accumulator with sine or four-sample polynomial BLEP saw. */
|
|
7
|
+
export declare const oscillator: import("@unworklet/core").SubgraphDecl<[config: OscillatorConfig], {
|
|
8
|
+
/** Emit current phase, then advance. Reset emits phase zero on that sample. */
|
|
9
|
+
tick(frequency: Node<"f32">, reset: Node<"bool">): Node<"f32">;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineSubgraph, f32, f64, select, state } from '@unworklet/core';
|
|
2
|
+
/** Phase accumulator with sine or four-sample polynomial BLEP saw. */
|
|
3
|
+
export const oscillator = defineSubgraph((config) => {
|
|
4
|
+
if (!Number.isFinite(config.sampleRate) || config.sampleRate < 8000 || config.sampleRate > 192000) {
|
|
5
|
+
throw new RangeError('oscillator sampleRate must be finite and in [8000, 192000] Hz');
|
|
6
|
+
}
|
|
7
|
+
if (config.waveform !== 'sine' && config.waveform !== 'saw')
|
|
8
|
+
throw new RangeError('unknown oscillator waveform');
|
|
9
|
+
const phase = state.f64(0).named('phase');
|
|
10
|
+
return {
|
|
11
|
+
/** Emit current phase, then advance. Reset emits phase zero on that sample. */
|
|
12
|
+
tick(frequency, reset) {
|
|
13
|
+
const dt = f64(frequency).clamp(0, 0.45 * config.sampleRate).div(config.sampleRate);
|
|
14
|
+
const p = select(reset, f64(0), phase.read());
|
|
15
|
+
phase.write(p.add(dt).frac());
|
|
16
|
+
if (config.waveform === 'sine') {
|
|
17
|
+
// Fold to [-pi/2, pi/2]; degree 13 has <7e-10 remainder here.
|
|
18
|
+
const cycles = select(p.lt(0.25), p, select(p.lt(0.75), f64(0.5).sub(p), p.sub(1)));
|
|
19
|
+
const angle = cycles.mul(2 * Math.PI), z = angle.mul(angle);
|
|
20
|
+
const polynomial = f64(1 / 6227020800).mul(z).sub(1 / 39916800).mul(z).add(1 / 362880).mul(z).sub(1 / 5040).mul(z).add(1 / 120).mul(z).sub(1 / 6).mul(z).add(1);
|
|
21
|
+
return f32(angle.mul(polynomial));
|
|
22
|
+
}
|
|
23
|
+
// A zero frequency holds phase; avoid 0/0 even in eagerly evaluated selects.
|
|
24
|
+
const width = select(dt.gt(0), dt, f64(1));
|
|
25
|
+
// Integrate a cubic B-spline kernel (four boxes convolved). Its step
|
|
26
|
+
// complement for x >= 0 has support two sample intervals.
|
|
27
|
+
const complement = (x) => {
|
|
28
|
+
const x2 = x.mul(x), outer = f64(2).sub(x).max(0);
|
|
29
|
+
return select(x.lt(1), f64(0.5).sub(x.mul(2 / 3)).add(x2.mul(x).div(3)).sub(x2.mul(x2).div(8)), outer.mul(outer).mul(outer).mul(outer).div(24));
|
|
30
|
+
};
|
|
31
|
+
const correction = complement(p.div(width)).sub(complement(f64(1).sub(p).div(width))).mul(2);
|
|
32
|
+
return f32(p.mul(2).sub(1).add(select(dt.gt(0), correction, f64(0))));
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Node } from '@unworklet/core';
|
|
2
|
+
import type { DriveCurve } from './drive.js';
|
|
3
|
+
/** Matched linear-phase bulk delay. FIR precursors exist before sample 32. */
|
|
4
|
+
export declare const OVERSAMPLED_DRIVE_LATENCY_SAMPLES = 32;
|
|
5
|
+
export interface OversampledDriveConfig {
|
|
6
|
+
/** Host rate. The fixed arithmetic shaper has no nested sample-rate runtime. */
|
|
7
|
+
sampleRate: number;
|
|
8
|
+
factor: 2 | 4;
|
|
9
|
+
curve: DriveCurve;
|
|
10
|
+
}
|
|
11
|
+
/** Fixed polyphase interpolation -> memoryless shape -> FIR/phase-zero
|
|
12
|
+
* decimation. Both factors have 32-sample matched bulk delay and deliberate
|
|
13
|
+
* high-frequency rolloff. Dry follows the exact linear resampling cascade.
|
|
14
|
+
*/
|
|
15
|
+
export declare const oversampledDrive: import("@unworklet/core").SubgraphDecl<[config: OversampledDriveConfig], {
|
|
16
|
+
/** Once per host sample. Input [-8,8], linear gain [0,32], mix [0,1].
|
|
17
|
+
* Reset invalidates old history before consuming the current sample. */
|
|
18
|
+
tick(input: Node<"f32">, gain: Node<"f32">, mix: Node<"f32">, reset: Node<"bool">): Node<"f32">;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { defineSubgraph, f32, f64, floor, i32, select, state } from '@unworklet/core';
|
|
2
|
+
/** Matched linear-phase bulk delay. FIR precursors exist before sample 32. */
|
|
3
|
+
export const OVERSAMPLED_DRIVE_LATENCY_SAMPLES = 32;
|
|
4
|
+
const SCALE = 2 ** 128;
|
|
5
|
+
function bounded(value, low, high) {
|
|
6
|
+
const wide = f64(value);
|
|
7
|
+
return select(wide.eq(wide), wide, f64(0)).clamp(low, high);
|
|
8
|
+
}
|
|
9
|
+
function soft(value) {
|
|
10
|
+
const x = value.clamp(-1, 1);
|
|
11
|
+
return x.mul(f64(1.5).sub(x.mul(x).mul(.5)));
|
|
12
|
+
}
|
|
13
|
+
function shape(x, curve) {
|
|
14
|
+
if (curve === 'hard')
|
|
15
|
+
return x.clamp(-1, 1);
|
|
16
|
+
if (curve === 'soft')
|
|
17
|
+
return soft(x);
|
|
18
|
+
if (curve === 'asymmetric')
|
|
19
|
+
return select(x.lt(0), soft(x.mul(2)).mul(.5), soft(x));
|
|
20
|
+
const phase = x.add(1).sub(floor(x.add(1).div(4)).mul(4));
|
|
21
|
+
return select(x.abs().lte(1), x, f64(1).sub(phase.sub(2).abs()));
|
|
22
|
+
}
|
|
23
|
+
function kernel(factor) {
|
|
24
|
+
const half = 16 * factor, cutoff = .45 / factor;
|
|
25
|
+
const values = Array.from({ length: 2 * half + 1 }, (_, k) => {
|
|
26
|
+
const distance = Math.abs(k - half);
|
|
27
|
+
if (distance === half)
|
|
28
|
+
return 0;
|
|
29
|
+
const ideal = distance === 0 ? 2 * cutoff : Math.sin(2 * Math.PI * cutoff * distance) / (Math.PI * distance);
|
|
30
|
+
return ideal * (.42 + .5 * Math.cos(Math.PI * distance / half) + .08 * Math.cos(2 * Math.PI * distance / half));
|
|
31
|
+
});
|
|
32
|
+
const sum = values.reduce((a, b) => a + b, 0);
|
|
33
|
+
return values.map(value => value / sum);
|
|
34
|
+
}
|
|
35
|
+
/** Fixed polyphase interpolation -> memoryless shape -> FIR/phase-zero
|
|
36
|
+
* decimation. Both factors have 32-sample matched bulk delay and deliberate
|
|
37
|
+
* high-frequency rolloff. Dry follows the exact linear resampling cascade.
|
|
38
|
+
*/
|
|
39
|
+
export const oversampledDrive = defineSubgraph((config) => {
|
|
40
|
+
const { sampleRate, factor, curve } = config;
|
|
41
|
+
if (!Number.isFinite(sampleRate) || sampleRate < 8000 || sampleRate > 192000)
|
|
42
|
+
throw new RangeError('oversampledDrive sampleRate must be in [8000,192000] Hz');
|
|
43
|
+
if (factor !== 2 && factor !== 4)
|
|
44
|
+
throw new RangeError('oversampledDrive factor must be 2 or 4');
|
|
45
|
+
if (!['hard', 'soft', 'asymmetric', 'fold'].includes(curve))
|
|
46
|
+
throw new RangeError('unknown oversampledDrive curve');
|
|
47
|
+
const h = kernel(factor), size = h.length;
|
|
48
|
+
const dryKernel = Array.from({ length: 65 }, (_, j) => factor * h.reduce((sum, value, k) => sum + value * (h[factor * j - k] ?? 0), 0));
|
|
49
|
+
const driven = state.buffer.f64({ size: 33 }).expose({ name: 'driven-scaled', snapshot: 'persistent' });
|
|
50
|
+
const raw = state.buffer.f64({ size: 65 }).expose({ name: 'dry-scaled', snapshot: 'persistent' });
|
|
51
|
+
const shaped = state.buffer.f64({ size }).expose({ name: 'shaped-scaled', snapshot: 'persistent' });
|
|
52
|
+
const drivenCursor = state.i32(0).named('driven-cursor'), dryCursor = state.i32(0).named('dry-cursor');
|
|
53
|
+
const highCursor = state.i32(0).named('high-cursor');
|
|
54
|
+
const baseValid = state.i32(0).named('base-valid'), highValid = state.i32(0).named('high-valid');
|
|
55
|
+
const up = state.f64(0).expose({ name: 'interpolated-scaled', snapshot: 'transient' });
|
|
56
|
+
const wet = state.f64(0).expose({ name: 'wet-sum-scaled', snapshot: 'transient' });
|
|
57
|
+
const dry = state.f64(0).expose({ name: 'dry-sum-scaled', snapshot: 'transient' });
|
|
58
|
+
return {
|
|
59
|
+
/** Once per host sample. Input [-8,8], linear gain [0,32], mix [0,1].
|
|
60
|
+
* Reset invalidates old history before consuming the current sample. */
|
|
61
|
+
tick(input, gain, mix, reset) {
|
|
62
|
+
const audio = bounded(input, -8, 8), amount = bounded(mix, 0, 1);
|
|
63
|
+
baseValid.write(select(reset, i32(1), baseValid.read().add(1).min(65)));
|
|
64
|
+
highValid.write(select(reset, i32(0), highValid.read()));
|
|
65
|
+
raw.write(dryCursor.read(), audio.mul(SCALE));
|
|
66
|
+
driven.write(drivenCursor.read(), audio.mul(bounded(gain, 0, 32)).mul(SCALE));
|
|
67
|
+
const baseAt = (lag) => select(baseValid.read().gt(lag), driven.read(drivenCursor.read().add(33 - lag).mod(33)), f64(0));
|
|
68
|
+
const highAt = (lag) => select(highValid.read().gt(lag), shaped.read(highCursor.read().add(size - lag).mod(size)), f64(0));
|
|
69
|
+
// These are fixed construction-time phases, not a second graph scheduler.
|
|
70
|
+
// Filtering suppresses images before and after the nonlinearity.
|
|
71
|
+
for (let phase = 0; phase < factor; phase++) {
|
|
72
|
+
up.write(0);
|
|
73
|
+
for (let tap = phase; tap < size; tap += factor) {
|
|
74
|
+
if (h[tap] !== 0)
|
|
75
|
+
up.write(up.read().add(baseAt((tap - phase) / factor).mul(factor * h[tap])));
|
|
76
|
+
}
|
|
77
|
+
shaped.write(highCursor.read(), shape(up.read().div(SCALE), curve).mul(SCALE));
|
|
78
|
+
highValid.write(highValid.read().add(1).min(size));
|
|
79
|
+
if (phase === 0) {
|
|
80
|
+
wet.write(0);
|
|
81
|
+
for (let tap = 0; tap < size; tap++)
|
|
82
|
+
if (h[tap] !== 0)
|
|
83
|
+
wet.write(wet.read().add(highAt(tap).mul(h[tap])));
|
|
84
|
+
}
|
|
85
|
+
highCursor.write(highCursor.read().add(1).mod(size));
|
|
86
|
+
}
|
|
87
|
+
dry.write(0);
|
|
88
|
+
for (let tap = 0; tap < dryKernel.length; tap++) {
|
|
89
|
+
if (dryKernel[tap] !== 0) {
|
|
90
|
+
const previous = select(baseValid.read().gt(tap), raw.read(dryCursor.read().add(65 - tap).mod(65)), f64(0));
|
|
91
|
+
dry.write(dry.read().add(previous.mul(dryKernel[tap])));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
drivenCursor.write(drivenCursor.read().add(1).mod(33));
|
|
95
|
+
dryCursor.write(dryCursor.read().add(1).mod(65));
|
|
96
|
+
return f32(dry.read().mul(f64(1).sub(amount)).add(wet.read().mul(amount)).div(SCALE));
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
});
|