@codexo/exojs 0.6.11 → 0.7.11
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 +1356 -0
- package/dist/esm/audio/AbstractMedia.d.ts +18 -0
- package/dist/esm/audio/AbstractMedia.js +66 -0
- package/dist/esm/audio/AbstractMedia.js.map +1 -1
- package/dist/esm/audio/AudioAnalyser.d.ts +62 -23
- package/dist/esm/audio/AudioAnalyser.js +261 -57
- package/dist/esm/audio/AudioAnalyser.js.map +1 -1
- package/dist/esm/audio/AudioBus.d.ts +45 -0
- package/dist/esm/audio/AudioBus.js +219 -0
- package/dist/esm/audio/AudioBus.js.map +1 -0
- package/dist/esm/audio/AudioFilter.d.ts +9 -0
- package/dist/esm/audio/AudioFilter.js +7 -0
- package/dist/esm/audio/AudioFilter.js.map +1 -0
- package/dist/esm/audio/AudioListener.d.ts +20 -0
- package/dist/esm/audio/AudioListener.js +86 -0
- package/dist/esm/audio/AudioListener.js.map +1 -0
- package/dist/esm/audio/AudioManager.d.ts +31 -0
- package/dist/esm/audio/AudioManager.js +102 -0
- package/dist/esm/audio/AudioManager.js.map +1 -0
- package/dist/esm/audio/BeatDetector.d.ts +121 -0
- package/dist/esm/audio/BeatDetector.js +936 -0
- package/dist/esm/audio/BeatDetector.js.map +1 -0
- package/dist/esm/audio/Envelope.d.ts +44 -0
- package/dist/esm/audio/Envelope.js +60 -0
- package/dist/esm/audio/Envelope.js.map +1 -0
- package/dist/esm/audio/Music.d.ts +8 -0
- package/dist/esm/audio/Music.js +33 -4
- package/dist/esm/audio/Music.js.map +1 -1
- package/dist/esm/audio/OscillatorSound.d.ts +98 -0
- package/dist/esm/audio/OscillatorSound.js +342 -0
- package/dist/esm/audio/OscillatorSound.js.map +1 -0
- package/dist/esm/audio/Sound.d.ts +94 -9
- package/dist/esm/audio/Sound.js +283 -117
- package/dist/esm/audio/Sound.js.map +1 -1
- package/dist/esm/audio/crossFade.d.ts +19 -0
- package/dist/esm/audio/crossFade.js +26 -0
- package/dist/esm/audio/crossFade.js.map +1 -0
- package/dist/esm/audio/dsp/fft.d.ts +22 -0
- package/dist/esm/audio/dsp/mel.d.ts +43 -0
- package/dist/esm/audio/dsp/tempogram.d.ts +51 -0
- package/dist/esm/audio/filters/ChorusFilter.d.ts +47 -0
- package/dist/esm/audio/filters/ChorusFilter.js +139 -0
- package/dist/esm/audio/filters/ChorusFilter.js.map +1 -0
- package/dist/esm/audio/filters/CompressorFilter.d.ts +31 -0
- package/dist/esm/audio/filters/CompressorFilter.js +97 -0
- package/dist/esm/audio/filters/CompressorFilter.js.map +1 -0
- package/dist/esm/audio/filters/DelayFilter.d.ts +23 -0
- package/dist/esm/audio/filters/DelayFilter.js +100 -0
- package/dist/esm/audio/filters/DelayFilter.js.map +1 -0
- package/dist/esm/audio/filters/DuckingFilter.d.ts +31 -0
- package/dist/esm/audio/filters/DuckingFilter.js +152 -0
- package/dist/esm/audio/filters/DuckingFilter.js.map +1 -0
- package/dist/esm/audio/filters/EqualizerFilter.d.ts +29 -0
- package/dist/esm/audio/filters/EqualizerFilter.js +94 -0
- package/dist/esm/audio/filters/EqualizerFilter.js.map +1 -0
- package/dist/esm/audio/filters/GranularFilter.d.ts +56 -0
- package/dist/esm/audio/filters/GranularFilter.js +170 -0
- package/dist/esm/audio/filters/GranularFilter.js.map +1 -0
- package/dist/esm/audio/filters/HighpassFilter.d.ts +19 -0
- package/dist/esm/audio/filters/HighpassFilter.js +62 -0
- package/dist/esm/audio/filters/HighpassFilter.js.map +1 -0
- package/dist/esm/audio/filters/LowpassFilter.d.ts +19 -0
- package/dist/esm/audio/filters/LowpassFilter.js +62 -0
- package/dist/esm/audio/filters/LowpassFilter.js.map +1 -0
- package/dist/esm/audio/filters/PitchShiftFilter.d.ts +42 -0
- package/dist/esm/audio/filters/PitchShiftFilter.js +130 -0
- package/dist/esm/audio/filters/PitchShiftFilter.js.map +1 -0
- package/dist/esm/audio/filters/ReverbFilter.d.ts +24 -0
- package/dist/esm/audio/filters/ReverbFilter.js +107 -0
- package/dist/esm/audio/filters/ReverbFilter.js.map +1 -0
- package/dist/esm/audio/filters/VocoderFilter.d.ts +38 -0
- package/dist/esm/audio/filters/VocoderFilter.js +163 -0
- package/dist/esm/audio/filters/VocoderFilter.js.map +1 -0
- package/dist/esm/audio/filters/WorkletFilter.d.ts +46 -0
- package/dist/esm/audio/filters/WorkletFilter.js +101 -0
- package/dist/esm/audio/filters/WorkletFilter.js.map +1 -0
- package/dist/esm/audio/filters/index.d.ts +12 -0
- package/dist/esm/audio/index.d.ts +15 -1
- package/dist/esm/audio/worklet/registerWorklet.d.ts +10 -0
- package/dist/esm/audio/worklet/registerWorklet.js +44 -0
- package/dist/esm/audio/worklet/registerWorklet.js.map +1 -0
- package/dist/esm/core/Application.d.ts +19 -0
- package/dist/esm/core/Application.js +76 -2
- package/dist/esm/core/Application.js.map +1 -1
- package/dist/esm/core/SceneNode.d.ts +9 -1
- package/dist/esm/core/SceneNode.js +44 -6
- package/dist/esm/core/SceneNode.js.map +1 -1
- package/dist/esm/core/Time.js +1 -1
- package/dist/esm/core/index.d.ts +0 -1
- package/dist/esm/debug/BoundingBoxesLayer.d.ts +18 -0
- package/dist/esm/debug/BoundingBoxesLayer.js +128 -0
- package/dist/esm/debug/BoundingBoxesLayer.js.map +1 -0
- package/dist/esm/debug/DebugLayer.d.ts +29 -0
- package/dist/esm/debug/DebugLayer.js +26 -0
- package/dist/esm/debug/DebugLayer.js.map +1 -0
- package/dist/esm/debug/DebugOverlay.d.ts +48 -0
- package/dist/esm/debug/DebugOverlay.js +117 -0
- package/dist/esm/debug/DebugOverlay.js.map +1 -0
- package/dist/esm/debug/HitTestLayer.d.ts +23 -0
- package/dist/esm/debug/HitTestLayer.js +109 -0
- package/dist/esm/debug/HitTestLayer.js.map +1 -0
- package/dist/esm/debug/PerformanceLayer.d.ts +21 -0
- package/dist/esm/debug/PerformanceLayer.js +175 -0
- package/dist/esm/debug/PerformanceLayer.js.map +1 -0
- package/dist/esm/debug/PointerStackLayer.d.ts +23 -0
- package/dist/esm/debug/PointerStackLayer.js +152 -0
- package/dist/esm/debug/PointerStackLayer.js.map +1 -0
- package/dist/esm/debug/index.d.ts +6 -0
- package/dist/esm/debug/index.js +7 -0
- package/dist/esm/debug/index.js.map +1 -0
- package/dist/esm/index.js +29 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/input/InputManager.d.ts +10 -0
- package/dist/esm/input/InputManager.js +35 -5
- package/dist/esm/input/InputManager.js.map +1 -1
- package/dist/esm/input/InteractionEvent.d.ts +18 -0
- package/dist/esm/input/InteractionEvent.js +29 -0
- package/dist/esm/input/InteractionEvent.js.map +1 -0
- package/dist/esm/input/InteractionManager.d.ts +134 -0
- package/dist/esm/input/InteractionManager.js +546 -0
- package/dist/esm/input/InteractionManager.js.map +1 -0
- package/dist/esm/input/index.d.ts +2 -0
- package/dist/esm/input/interaction-hooks.d.ts +34 -0
- package/dist/esm/input/interaction-hooks.js +35 -0
- package/dist/esm/input/interaction-hooks.js.map +1 -0
- package/dist/esm/math/Circle.d.ts +12 -2
- package/dist/esm/math/Circle.js +82 -14
- package/dist/esm/math/Circle.js.map +1 -1
- package/dist/esm/math/Interval.js +1 -1
- package/dist/esm/math/ObservableVector.d.ts +2 -2
- package/dist/esm/math/ObservableVector.js +4 -2
- package/dist/esm/math/ObservableVector.js.map +1 -1
- package/dist/esm/math/Polygon.d.ts +15 -1
- package/dist/esm/math/Polygon.js +58 -6
- package/dist/esm/math/Polygon.js.map +1 -1
- package/dist/esm/math/Quadtree.d.ts +47 -0
- package/dist/esm/math/Quadtree.js +168 -0
- package/dist/esm/math/Quadtree.js.map +1 -0
- package/dist/esm/math/Random.js +1 -1
- package/dist/esm/math/Size.js +1 -1
- package/dist/esm/math/Vector.js +1 -1
- package/dist/esm/math/collision-detection.js +4 -1
- package/dist/esm/math/collision-detection.js.map +1 -1
- package/dist/esm/math/index.d.ts +2 -0
- package/dist/esm/math/swept-collision.d.ts +90 -0
- package/dist/esm/math/swept-collision.js +255 -0
- package/dist/esm/math/swept-collision.js.map +1 -0
- package/dist/esm/particles/ParticleSystem.js +1 -0
- package/dist/esm/particles/ParticleSystem.js.map +1 -1
- package/dist/esm/particles/affectors/TorqueAffector.js +1 -1
- package/dist/esm/rendering/Container.d.ts +1 -0
- package/dist/esm/rendering/Container.js +19 -0
- package/dist/esm/rendering/Container.js.map +1 -1
- package/dist/esm/rendering/RenderNode.d.ts +27 -0
- package/dist/esm/rendering/RenderNode.js +44 -0
- package/dist/esm/rendering/RenderNode.js.map +1 -1
- package/dist/esm/rendering/View.d.ts +6 -4
- package/dist/esm/rendering/View.js +12 -2
- package/dist/esm/rendering/View.js.map +1 -1
- package/dist/esm/rendering/filters/WebGl2ShaderFilter.d.ts +109 -0
- package/dist/esm/rendering/filters/WebGl2ShaderFilter.js +268 -0
- package/dist/esm/rendering/filters/WebGl2ShaderFilter.js.map +1 -0
- package/dist/esm/rendering/filters/WebGpuShaderFilter.d.ts +111 -0
- package/dist/esm/rendering/filters/WebGpuShaderFilter.js +397 -0
- package/dist/esm/rendering/filters/WebGpuShaderFilter.js.map +1 -0
- package/dist/esm/rendering/index.d.ts +3 -0
- package/dist/esm/rendering/mesh/Mesh.js +1 -0
- package/dist/esm/rendering/mesh/Mesh.js.map +1 -1
- package/dist/esm/rendering/shader/upgradeFragmentShaderToGl300.d.ts +34 -0
- package/dist/esm/rendering/shader/upgradeFragmentShaderToGl300.js +60 -0
- package/dist/esm/rendering/shader/upgradeFragmentShaderToGl300.js.map +1 -0
- package/dist/esm/rendering/sprite/Sprite.d.ts +6 -1
- package/dist/esm/rendering/sprite/Sprite.js +41 -19
- package/dist/esm/rendering/sprite/Sprite.js.map +1 -1
- package/dist/esm/rendering/video/Video.d.ts +4 -0
- package/dist/esm/rendering/video/Video.js +32 -4
- package/dist/esm/rendering/video/Video.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2Backend.d.ts +4 -4
- package/dist/esm/rendering/webgl2/WebGl2Backend.js +7 -16
- package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuBackend.d.ts +10 -8
- package/dist/esm/rendering/webgpu/WebGpuBackend.js +30 -40
- package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
- package/dist/exo.esm.js +8021 -2459
- package/dist/exo.esm.js.map +1 -1
- package/package.json +14 -2
- package/dist/esm/core/Quadtree.d.ts +0 -20
- package/dist/esm/core/Quadtree.js +0 -86
- package/dist/esm/core/Quadtree.js.map +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isAudioContextReady, getAudioContext, onAudioContextReady } from '../audio-context.js';
|
|
2
|
+
import { AudioFilter } from '../AudioFilter.js';
|
|
3
|
+
|
|
4
|
+
class LowpassFilter extends AudioFilter {
|
|
5
|
+
_node = null;
|
|
6
|
+
_frequency;
|
|
7
|
+
_resonance;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
super();
|
|
10
|
+
this._frequency = options.frequency ?? 1000;
|
|
11
|
+
this._resonance = options.resonance ?? 1;
|
|
12
|
+
if (isAudioContextReady()) {
|
|
13
|
+
this._setup(getAudioContext());
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
onAudioContextReady.once(this._setup, this);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
get inputNode() {
|
|
20
|
+
if (!this._node)
|
|
21
|
+
throw new Error('LowpassFilter not yet initialized.');
|
|
22
|
+
return this._node;
|
|
23
|
+
}
|
|
24
|
+
get outputNode() {
|
|
25
|
+
if (!this._node)
|
|
26
|
+
throw new Error('LowpassFilter not yet initialized.');
|
|
27
|
+
return this._node;
|
|
28
|
+
}
|
|
29
|
+
get frequency() {
|
|
30
|
+
return this._frequency;
|
|
31
|
+
}
|
|
32
|
+
set frequency(value) {
|
|
33
|
+
this._frequency = Math.max(20, Math.min(20000, value));
|
|
34
|
+
if (this._node) {
|
|
35
|
+
this._node.frequency.setTargetAtTime(this._frequency, this._node.context.currentTime, 0.01);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
get resonance() {
|
|
39
|
+
return this._resonance;
|
|
40
|
+
}
|
|
41
|
+
set resonance(value) {
|
|
42
|
+
this._resonance = Math.max(0.0001, value);
|
|
43
|
+
if (this._node) {
|
|
44
|
+
this._node.Q.setTargetAtTime(this._resonance, this._node.context.currentTime, 0.01);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
destroy() {
|
|
48
|
+
onAudioContextReady.clearByContext(this);
|
|
49
|
+
this._node?.disconnect();
|
|
50
|
+
this._node = null;
|
|
51
|
+
}
|
|
52
|
+
_setup(ctx) {
|
|
53
|
+
const node = ctx.createBiquadFilter();
|
|
54
|
+
node.type = 'lowpass';
|
|
55
|
+
node.frequency.setValueAtTime(this._frequency, ctx.currentTime);
|
|
56
|
+
node.Q.setValueAtTime(this._resonance, ctx.currentTime);
|
|
57
|
+
this._node = node;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { LowpassFilter };
|
|
62
|
+
//# sourceMappingURL=LowpassFilter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LowpassFilter.js","sources":["../../../../../src/audio/filters/LowpassFilter.ts"],"sourcesContent":[null],"names":[],"mappings":";;;AAQM,MAAO,aAAc,SAAQ,WAAW,CAAA;IAClC,KAAK,GAA4B,IAAI;AACrC,IAAA,UAAU;AACV,IAAA,UAAU;AAElB,IAAA,WAAA,CAAmB,UAAgC,EAAE,EAAA;AACjD,QAAA,KAAK,EAAE;QACP,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI;QAC3C,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,SAAS,IAAI,CAAC;QACxC,IAAI,mBAAmB,EAAE,EAAE;AACvB,YAAA,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;QAClC;aAAO;YACH,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;QAC/C;IACJ;AAEA,IAAA,IAAW,SAAS,GAAA;QAChB,IAAI,CAAC,IAAI,CAAC,KAAK;AAAE,YAAA,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC;QACtE,OAAO,IAAI,CAAC,KAAK;IACrB;AAEA,IAAA,IAAW,UAAU,GAAA;QACjB,IAAI,CAAC,IAAI,CAAC,KAAK;AAAE,YAAA,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC;QACtE,OAAO,IAAI,CAAC,KAAK;IACrB;AAEA,IAAA,IAAW,SAAS,GAAA;QAChB,OAAO,IAAI,CAAC,UAAU;IAC1B;IAEA,IAAW,SAAS,CAAC,KAAa,EAAA;AAC9B,QAAA,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AACtD,QAAA,IAAI,IAAI,CAAC,KAAK,EAAE;YACZ,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,eAAe,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC;QAC/F;IACJ;AAEA,IAAA,IAAW,SAAS,GAAA;QAChB,OAAO,IAAI,CAAC,UAAU;IAC1B;IAEA,IAAW,SAAS,CAAC,KAAa,EAAA;QAC9B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC;AACzC,QAAA,IAAI,IAAI,CAAC,KAAK,EAAE;YACZ,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC;QACvF;IACJ;IAEgB,OAAO,GAAA;AACnB,QAAA,mBAAmB,CAAC,cAAc,CAAC,IAAI,CAAC;AACxC,QAAA,IAAI,CAAC,KAAK,EAAE,UAAU,EAAE;AACxB,QAAA,IAAI,CAAC,KAAK,GAAG,IAAI;IACrB;AAEQ,IAAA,MAAM,CAAC,GAAiB,EAAA;AAC5B,QAAA,MAAM,IAAI,GAAG,GAAG,CAAC,kBAAkB,EAAE;AACrC,QAAA,IAAI,CAAC,IAAI,GAAG,SAAS;AACrB,QAAA,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,WAAW,CAAC;AAC/D,QAAA,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,WAAW,CAAC;AACvD,QAAA,IAAI,CAAC,KAAK,GAAG,IAAI;IACrB;AACH;;;;"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { WorkletFilter } from './WorkletFilter';
|
|
2
|
+
export interface PitchShiftFilterOptions {
|
|
3
|
+
/** Pitch ratio. 1.0 = no change, 0.5 = octave down, 2.0 = octave up. Default 1.0. */
|
|
4
|
+
pitch?: number;
|
|
5
|
+
/** Dry/wet mix, 0..1. Default 1.0 (full wet). */
|
|
6
|
+
wet?: number;
|
|
7
|
+
/**
|
|
8
|
+
* Internal grain size in samples. Default 1024 (~21ms at 48kHz).
|
|
9
|
+
* Larger = more delay but cleaner pitch shifting.
|
|
10
|
+
*/
|
|
11
|
+
grainSize?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Real-time pitch shifter via granular synthesis (WorkletFilter).
|
|
15
|
+
*
|
|
16
|
+
* Quality: good for ±1 octave (pitch 0.5x-2.0x). Beyond that, audible
|
|
17
|
+
* artifacts (graininess, phase issues). For high-quality pitch shift,
|
|
18
|
+
* a phase-vocoder approach is required and not available in V1.
|
|
19
|
+
*
|
|
20
|
+
* Latency: ~half-grain-size = ~10ms at default 1024-sample grains
|
|
21
|
+
* (at 48kHz sample rate). Not suitable for live monitoring; fine for
|
|
22
|
+
* games / playback.
|
|
23
|
+
*
|
|
24
|
+
* Use cases:
|
|
25
|
+
* - Sound variation: random ±200 cent pitch on each footstep / bullet
|
|
26
|
+
* - Voice effects: chipmunk (1.5x) or demon (0.7x) for game NPCs
|
|
27
|
+
* - Detune layering: stack 0.99x and 1.01x for thick chorused sound
|
|
28
|
+
*/
|
|
29
|
+
export declare class PitchShiftFilter extends WorkletFilter {
|
|
30
|
+
private _pitch;
|
|
31
|
+
private _wet;
|
|
32
|
+
private readonly _grainSize;
|
|
33
|
+
constructor(options?: PitchShiftFilterOptions);
|
|
34
|
+
protected get _workletName(): string;
|
|
35
|
+
protected get _workletSource(): string;
|
|
36
|
+
protected get _workletOptions(): AudioWorkletNodeOptions;
|
|
37
|
+
protected _onWorkletReady(): void;
|
|
38
|
+
get pitch(): number;
|
|
39
|
+
set pitch(value: number);
|
|
40
|
+
get wet(): number;
|
|
41
|
+
set wet(value: number);
|
|
42
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { WorkletFilter } from './WorkletFilter.js';
|
|
2
|
+
|
|
3
|
+
const pitchShiftWorkletSource = `
|
|
4
|
+
class PitchShiftProcessor extends AudioWorkletProcessor {
|
|
5
|
+
static get parameterDescriptors() {
|
|
6
|
+
return [
|
|
7
|
+
{ name: 'pitch', defaultValue: 1.0, minValue: 0.25, maxValue: 4.0, automationRate: 'k-rate' },
|
|
8
|
+
{ name: 'wet', defaultValue: 1.0, minValue: 0, maxValue: 1.0, automationRate: 'k-rate' },
|
|
9
|
+
];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super();
|
|
14
|
+
const grainSize = options.processorOptions?.grainSize ?? 1024;
|
|
15
|
+
this._grainSize = grainSize;
|
|
16
|
+
this._bufferLength = grainSize * 4;
|
|
17
|
+
this._buffer = new Float32Array(this._bufferLength);
|
|
18
|
+
this._writePos = 0;
|
|
19
|
+
// Two staggered read positions for overlap-add
|
|
20
|
+
this._readPosA = 0;
|
|
21
|
+
this._readPosB = grainSize / 2;
|
|
22
|
+
this._hannWindow = this._buildHannWindow(grainSize);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_buildHannWindow(n) {
|
|
26
|
+
const w = new Float32Array(n);
|
|
27
|
+
for (let i = 0; i < n; i++) {
|
|
28
|
+
w[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / (n - 1)));
|
|
29
|
+
}
|
|
30
|
+
return w;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_readGrain(readPos) {
|
|
34
|
+
const grainSize = this._grainSize;
|
|
35
|
+
const idx = Math.floor(readPos);
|
|
36
|
+
const phase = idx % grainSize; // position within the grain envelope
|
|
37
|
+
const win = this._hannWindow[phase];
|
|
38
|
+
const bufIdx = ((this._writePos - this._bufferLength + idx) % this._bufferLength + this._bufferLength) % this._bufferLength;
|
|
39
|
+
return this._buffer[bufIdx] * win;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process(inputs, outputs, parameters) {
|
|
43
|
+
const input = inputs[0]?.[0];
|
|
44
|
+
const output = outputs[0]?.[0];
|
|
45
|
+
if (!input || !output) return true;
|
|
46
|
+
|
|
47
|
+
const pitch = parameters.pitch[0];
|
|
48
|
+
const wet = parameters.wet[0];
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < input.length; i++) {
|
|
51
|
+
// Write to circular buffer
|
|
52
|
+
this._buffer[this._writePos] = input[i];
|
|
53
|
+
this._writePos = (this._writePos + 1) % this._bufferLength;
|
|
54
|
+
|
|
55
|
+
// Read two grains and sum
|
|
56
|
+
const grainA = this._readGrain(this._readPosA);
|
|
57
|
+
const grainB = this._readGrain(this._readPosB);
|
|
58
|
+
const shifted = grainA + grainB;
|
|
59
|
+
|
|
60
|
+
// Mix with dry
|
|
61
|
+
output[i] = (1 - wet) * input[i] + wet * shifted;
|
|
62
|
+
|
|
63
|
+
// Advance read positions at pitch rate
|
|
64
|
+
this._readPosA += pitch;
|
|
65
|
+
this._readPosB += pitch;
|
|
66
|
+
if (this._readPosA >= this._grainSize) this._readPosA -= this._grainSize;
|
|
67
|
+
if (this._readPosB >= this._grainSize) this._readPosB -= this._grainSize;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
registerProcessor('exojs-pitch-shift', PitchShiftProcessor);
|
|
73
|
+
`;
|
|
74
|
+
/**
|
|
75
|
+
* Real-time pitch shifter via granular synthesis (WorkletFilter).
|
|
76
|
+
*
|
|
77
|
+
* Quality: good for ±1 octave (pitch 0.5x-2.0x). Beyond that, audible
|
|
78
|
+
* artifacts (graininess, phase issues). For high-quality pitch shift,
|
|
79
|
+
* a phase-vocoder approach is required and not available in V1.
|
|
80
|
+
*
|
|
81
|
+
* Latency: ~half-grain-size = ~10ms at default 1024-sample grains
|
|
82
|
+
* (at 48kHz sample rate). Not suitable for live monitoring; fine for
|
|
83
|
+
* games / playback.
|
|
84
|
+
*
|
|
85
|
+
* Use cases:
|
|
86
|
+
* - Sound variation: random ±200 cent pitch on each footstep / bullet
|
|
87
|
+
* - Voice effects: chipmunk (1.5x) or demon (0.7x) for game NPCs
|
|
88
|
+
* - Detune layering: stack 0.99x and 1.01x for thick chorused sound
|
|
89
|
+
*/
|
|
90
|
+
class PitchShiftFilter extends WorkletFilter {
|
|
91
|
+
_pitch;
|
|
92
|
+
_wet;
|
|
93
|
+
_grainSize;
|
|
94
|
+
constructor(options = {}) {
|
|
95
|
+
super();
|
|
96
|
+
this._pitch = Math.max(0.25, Math.min(4.0, options.pitch ?? 1.0));
|
|
97
|
+
this._wet = Math.max(0, Math.min(1.0, options.wet ?? 1.0));
|
|
98
|
+
this._grainSize = options.grainSize ?? 1024;
|
|
99
|
+
}
|
|
100
|
+
get _workletName() {
|
|
101
|
+
return 'exojs-pitch-shift';
|
|
102
|
+
}
|
|
103
|
+
get _workletSource() {
|
|
104
|
+
return pitchShiftWorkletSource;
|
|
105
|
+
}
|
|
106
|
+
get _workletOptions() {
|
|
107
|
+
return {
|
|
108
|
+
numberOfInputs: 1,
|
|
109
|
+
numberOfOutputs: 1,
|
|
110
|
+
processorOptions: { grainSize: this._grainSize },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
_onWorkletReady() {
|
|
114
|
+
this._setAudioParam('pitch', this._pitch);
|
|
115
|
+
this._setAudioParam('wet', this._wet);
|
|
116
|
+
}
|
|
117
|
+
get pitch() { return this._pitch; }
|
|
118
|
+
set pitch(value) {
|
|
119
|
+
this._pitch = Math.max(0.25, Math.min(4.0, value));
|
|
120
|
+
this._setAudioParam('pitch', this._pitch);
|
|
121
|
+
}
|
|
122
|
+
get wet() { return this._wet; }
|
|
123
|
+
set wet(value) {
|
|
124
|
+
this._wet = Math.max(0, Math.min(1.0, value));
|
|
125
|
+
this._setAudioParam('wet', this._wet);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { PitchShiftFilter };
|
|
130
|
+
//# sourceMappingURL=PitchShiftFilter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PitchShiftFilter.js","sources":["../../../../../src/audio/filters/PitchShiftFilter.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAEA,MAAM,uBAAuB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsE/B;AAcD;;;;;;;;;;;;;;;AAeG;AACG,MAAO,gBAAiB,SAAQ,aAAa,CAAA;AACvC,IAAA,MAAM;AACN,IAAA,IAAI;AACK,IAAA,UAAU;AAE3B,IAAA,WAAA,CAAmB,UAAmC,EAAE,EAAA;AACpD,QAAA,KAAK,EAAE;QACP,IAAI,CAAC,MAAM,GAAO,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,GAAS,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;QAChE,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI;IAC/C;AAEA,IAAA,IAAc,YAAY,GAAA;AACtB,QAAA,OAAO,mBAAmB;IAC9B;AAEA,IAAA,IAAc,cAAc,GAAA;AACxB,QAAA,OAAO,uBAAuB;IAClC;AAEA,IAAA,IAAuB,eAAe,GAAA;QAClC,OAAO;AACH,YAAA,cAAc,EAAE,CAAC;AACjB,YAAA,eAAe,EAAE,CAAC;AAClB,YAAA,gBAAgB,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE;SACnD;IACL;IAEmB,eAAe,GAAA;QAC9B,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC;QACzC,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC;IACzC;IAEA,IAAW,KAAK,KAAa,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;IACjD,IAAW,KAAK,CAAC,KAAa,EAAA;AAC1B,QAAA,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAClD,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC;IAC7C;IAEA,IAAW,GAAG,KAAa,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7C,IAAW,GAAG,CAAC,KAAa,EAAA;AACxB,QAAA,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC7C,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC;IACzC;AACH;;;;"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AudioFilter } from '../AudioFilter';
|
|
2
|
+
export interface ReverbFilterOptions {
|
|
3
|
+
durationSeconds?: number;
|
|
4
|
+
decay?: number;
|
|
5
|
+
wet?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class ReverbFilter extends AudioFilter {
|
|
8
|
+
private _setup;
|
|
9
|
+
private _duration;
|
|
10
|
+
private _decay;
|
|
11
|
+
private _wet;
|
|
12
|
+
constructor(options?: ReverbFilterOptions);
|
|
13
|
+
get inputNode(): AudioNode;
|
|
14
|
+
get outputNode(): AudioNode;
|
|
15
|
+
get durationSeconds(): number;
|
|
16
|
+
set durationSeconds(value: number);
|
|
17
|
+
get decay(): number;
|
|
18
|
+
set decay(value: number);
|
|
19
|
+
get wet(): number;
|
|
20
|
+
set wet(value: number);
|
|
21
|
+
destroy(): void;
|
|
22
|
+
private _generateImpulseResponse;
|
|
23
|
+
private _setupNodes;
|
|
24
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { isAudioContextReady, getAudioContext, onAudioContextReady } from '../audio-context.js';
|
|
2
|
+
import { AudioFilter } from '../AudioFilter.js';
|
|
3
|
+
|
|
4
|
+
class ReverbFilter extends AudioFilter {
|
|
5
|
+
_setup = null;
|
|
6
|
+
_duration;
|
|
7
|
+
_decay;
|
|
8
|
+
_wet;
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
super();
|
|
11
|
+
this._duration = Math.max(0.1, Math.min(5, options.durationSeconds ?? 2));
|
|
12
|
+
this._decay = Math.max(0.5, Math.min(10, options.decay ?? 2));
|
|
13
|
+
this._wet = Math.max(0, Math.min(1, options.wet ?? 0.4));
|
|
14
|
+
if (isAudioContextReady()) {
|
|
15
|
+
this._setupNodes(getAudioContext());
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
onAudioContextReady.once(this._setupNodes, this);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
get inputNode() {
|
|
22
|
+
if (!this._setup)
|
|
23
|
+
throw new Error('ReverbFilter not yet initialized.');
|
|
24
|
+
return this._setup.inputGain;
|
|
25
|
+
}
|
|
26
|
+
get outputNode() {
|
|
27
|
+
if (!this._setup)
|
|
28
|
+
throw new Error('ReverbFilter not yet initialized.');
|
|
29
|
+
return this._setup.outputGain;
|
|
30
|
+
}
|
|
31
|
+
get durationSeconds() {
|
|
32
|
+
return this._duration;
|
|
33
|
+
}
|
|
34
|
+
set durationSeconds(value) {
|
|
35
|
+
this._duration = Math.max(0.1, Math.min(5, value));
|
|
36
|
+
if (this._setup) {
|
|
37
|
+
this._setup.convolver.buffer = this._generateImpulseResponse(this._setup.convolver.context);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
get decay() {
|
|
41
|
+
return this._decay;
|
|
42
|
+
}
|
|
43
|
+
set decay(value) {
|
|
44
|
+
this._decay = Math.max(0.5, Math.min(10, value));
|
|
45
|
+
if (this._setup) {
|
|
46
|
+
this._setup.convolver.buffer = this._generateImpulseResponse(this._setup.convolver.context);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
get wet() {
|
|
50
|
+
return this._wet;
|
|
51
|
+
}
|
|
52
|
+
set wet(value) {
|
|
53
|
+
this._wet = Math.max(0, Math.min(1, value));
|
|
54
|
+
if (this._setup) {
|
|
55
|
+
const ctx = this._setup.wetGain.context;
|
|
56
|
+
this._setup.wetGain.gain.setTargetAtTime(this._wet, ctx.currentTime, 0.01);
|
|
57
|
+
this._setup.dryGain.gain.setTargetAtTime(1 - this._wet, ctx.currentTime, 0.01);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
destroy() {
|
|
61
|
+
onAudioContextReady.clearByContext(this);
|
|
62
|
+
if (this._setup) {
|
|
63
|
+
this._setup.inputGain.disconnect();
|
|
64
|
+
this._setup.convolver.disconnect();
|
|
65
|
+
this._setup.dryGain.disconnect();
|
|
66
|
+
this._setup.wetGain.disconnect();
|
|
67
|
+
this._setup.outputGain.disconnect();
|
|
68
|
+
this._setup = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
_generateImpulseResponse(ctx) {
|
|
72
|
+
const length = Math.floor(ctx.sampleRate * this._duration);
|
|
73
|
+
const buffer = ctx.createBuffer(2, length, ctx.sampleRate);
|
|
74
|
+
for (let channel = 0; channel < 2; channel++) {
|
|
75
|
+
const data = buffer.getChannelData(channel);
|
|
76
|
+
for (let i = 0; i < length; i++) {
|
|
77
|
+
const t = i / length;
|
|
78
|
+
const decayFactor = Math.pow(1 - t, this._decay);
|
|
79
|
+
data[i] = (Math.random() * 2 - 1) * decayFactor;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return buffer;
|
|
83
|
+
}
|
|
84
|
+
_setupNodes(ctx) {
|
|
85
|
+
const inputGain = ctx.createGain();
|
|
86
|
+
const outputGain = ctx.createGain();
|
|
87
|
+
const convolver = ctx.createConvolver();
|
|
88
|
+
const dryGain = ctx.createGain();
|
|
89
|
+
const wetGain = ctx.createGain();
|
|
90
|
+
inputGain.gain.setValueAtTime(1, ctx.currentTime);
|
|
91
|
+
outputGain.gain.setValueAtTime(1, ctx.currentTime);
|
|
92
|
+
dryGain.gain.setValueAtTime(1 - this._wet, ctx.currentTime);
|
|
93
|
+
wetGain.gain.setValueAtTime(this._wet, ctx.currentTime);
|
|
94
|
+
convolver.buffer = this._generateImpulseResponse(ctx);
|
|
95
|
+
// Dry path: input → dryGain → output
|
|
96
|
+
inputGain.connect(dryGain);
|
|
97
|
+
dryGain.connect(outputGain);
|
|
98
|
+
// Wet path: input → convolver → wetGain → output
|
|
99
|
+
inputGain.connect(convolver);
|
|
100
|
+
convolver.connect(wetGain);
|
|
101
|
+
wetGain.connect(outputGain);
|
|
102
|
+
this._setup = { inputGain, outputGain, convolver, dryGain, wetGain };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { ReverbFilter };
|
|
107
|
+
//# sourceMappingURL=ReverbFilter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReverbFilter.js","sources":["../../../../../src/audio/filters/ReverbFilter.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { WorkletFilter } from './WorkletFilter';
|
|
2
|
+
import type { AudioBus } from '../AudioBus';
|
|
3
|
+
export interface VocoderFilterOptions {
|
|
4
|
+
/** Modulator AudioBus — its output drives the spectral envelope.
|
|
5
|
+
* Typically routed from a microphone or voice sample. */
|
|
6
|
+
modulator: AudioBus;
|
|
7
|
+
/** Number of frequency bands. More bands = better resolution, more CPU. Default 16. */
|
|
8
|
+
numBands?: number;
|
|
9
|
+
/** Lowest band center frequency in Hz. Default 80. */
|
|
10
|
+
minHz?: number;
|
|
11
|
+
/** Highest band center frequency in Hz. Default 8000. */
|
|
12
|
+
maxHz?: number;
|
|
13
|
+
/** Bandpass Q factor. Higher = narrower bands. Default 4. */
|
|
14
|
+
bandQ?: number;
|
|
15
|
+
/** Dry/wet mix, 0..1. Default 1.0. */
|
|
16
|
+
wet?: number;
|
|
17
|
+
/** Envelope follower smoothing factor (one-pole coefficient).
|
|
18
|
+
* Smaller = smoother / slower. Default 0.005. */
|
|
19
|
+
envelopeSmoothing?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class VocoderFilter extends WorkletFilter {
|
|
22
|
+
private readonly _modulator;
|
|
23
|
+
private readonly _numBands;
|
|
24
|
+
private readonly _minHz;
|
|
25
|
+
private readonly _maxHz;
|
|
26
|
+
private readonly _bandQ;
|
|
27
|
+
private _wet;
|
|
28
|
+
private _envelopeSmoothing;
|
|
29
|
+
constructor(options: VocoderFilterOptions);
|
|
30
|
+
protected get _workletName(): string;
|
|
31
|
+
protected get _workletSource(): string;
|
|
32
|
+
protected get _workletOptions(): AudioWorkletNodeOptions;
|
|
33
|
+
protected _onWorkletReady(audioContext: AudioContext): void;
|
|
34
|
+
get wet(): number;
|
|
35
|
+
set wet(value: number);
|
|
36
|
+
get envelopeSmoothing(): number;
|
|
37
|
+
set envelopeSmoothing(value: number);
|
|
38
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { WorkletFilter } from './WorkletFilter.js';
|
|
2
|
+
|
|
3
|
+
const vocoderWorkletSource = `
|
|
4
|
+
const sampleRate = globalThis.sampleRate;
|
|
5
|
+
|
|
6
|
+
class VocoderProcessor extends AudioWorkletProcessor {
|
|
7
|
+
static get parameterDescriptors() {
|
|
8
|
+
return [
|
|
9
|
+
{ name: 'wet', defaultValue: 1.0, minValue: 0, maxValue: 1.0, automationRate: 'k-rate' },
|
|
10
|
+
{ name: 'envelopeSmoothing', defaultValue: 0.005, minValue: 0.0001, maxValue: 0.1, automationRate: 'k-rate' },
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
constructor(options) {
|
|
15
|
+
super();
|
|
16
|
+
const opts = options.processorOptions ?? {};
|
|
17
|
+
const numBands = opts.numBands ?? 16;
|
|
18
|
+
const minHz = opts.minHz ?? 80;
|
|
19
|
+
const maxHz = opts.maxHz ?? 8000;
|
|
20
|
+
const Q = opts.bandQ ?? 4;
|
|
21
|
+
|
|
22
|
+
// Log-spaced band centers + biquad coefficients
|
|
23
|
+
this._bands = [];
|
|
24
|
+
for (let i = 0; i < numBands; i++) {
|
|
25
|
+
const ratio = numBands === 1 ? 0 : i / (numBands - 1);
|
|
26
|
+
const centerHz = minHz * Math.pow(maxHz / minHz, ratio);
|
|
27
|
+
const omega = 2 * Math.PI * centerHz / sampleRate;
|
|
28
|
+
const cos = Math.cos(omega);
|
|
29
|
+
const sin = Math.sin(omega);
|
|
30
|
+
const alpha = sin / (2 * Q);
|
|
31
|
+
|
|
32
|
+
// Bandpass (constant 0 dB peak) biquad
|
|
33
|
+
const a0 = 1 + alpha;
|
|
34
|
+
const b0 = alpha / a0;
|
|
35
|
+
const b1 = 0;
|
|
36
|
+
const b2 = -alpha / a0;
|
|
37
|
+
const a1 = (-2 * cos) / a0;
|
|
38
|
+
const a2 = (1 - alpha) / a0;
|
|
39
|
+
this._bands.push({ b0, b1, b2, a1, a2 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Per-band biquad state (one for carrier, one for modulator)
|
|
43
|
+
this._carrierStates = this._bands.map(() => ({ x1: 0, x2: 0, y1: 0, y2: 0 }));
|
|
44
|
+
this._modulatorStates = this._bands.map(() => ({ x1: 0, x2: 0, y1: 0, y2: 0 }));
|
|
45
|
+
|
|
46
|
+
// Per-band envelope follower
|
|
47
|
+
this._envelopes = new Float32Array(numBands);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_processBiquad(state, coef, x) {
|
|
51
|
+
const y = coef.b0 * x + coef.b1 * state.x1 + coef.b2 * state.x2 - coef.a1 * state.y1 - coef.a2 * state.y2;
|
|
52
|
+
state.x2 = state.x1; state.x1 = x;
|
|
53
|
+
state.y2 = state.y1; state.y1 = y;
|
|
54
|
+
return y;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process(inputs, outputs, parameters) {
|
|
58
|
+
const carrier = inputs[0]?.[0];
|
|
59
|
+
const modulator = inputs[1]?.[0];
|
|
60
|
+
const output = outputs[0]?.[0];
|
|
61
|
+
if (!carrier || !output) return true;
|
|
62
|
+
|
|
63
|
+
const wet = parameters.wet[0];
|
|
64
|
+
const envSmoothing = parameters.envelopeSmoothing[0];
|
|
65
|
+
const numBands = this._bands.length;
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < carrier.length; i++) {
|
|
68
|
+
const carrierSample = carrier[i];
|
|
69
|
+
const modulatorSample = modulator?.[i] ?? 0;
|
|
70
|
+
|
|
71
|
+
let bandSum = 0;
|
|
72
|
+
for (let b = 0; b < numBands; b++) {
|
|
73
|
+
const coef = this._bands[b];
|
|
74
|
+
|
|
75
|
+
// Modulator band → envelope follower
|
|
76
|
+
const modBand = this._processBiquad(this._modulatorStates[b], coef, modulatorSample);
|
|
77
|
+
const target = Math.abs(modBand);
|
|
78
|
+
this._envelopes[b] += (target - this._envelopes[b]) * envSmoothing;
|
|
79
|
+
|
|
80
|
+
// Carrier band, scaled by modulator envelope
|
|
81
|
+
const carBand = this._processBiquad(this._carrierStates[b], coef, carrierSample);
|
|
82
|
+
bandSum += carBand * this._envelopes[b];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
output[i] = (1 - wet) * carrierSample + wet * bandSum;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
registerProcessor('exojs-vocoder', VocoderProcessor);
|
|
91
|
+
`;
|
|
92
|
+
class VocoderFilter extends WorkletFilter {
|
|
93
|
+
// Declared nullable because super() may trigger _onWorkletReady before the
|
|
94
|
+
// subclass constructor body runs (if construction is aborted by a throw).
|
|
95
|
+
_modulator = null;
|
|
96
|
+
_numBands;
|
|
97
|
+
_minHz;
|
|
98
|
+
_maxHz;
|
|
99
|
+
_bandQ;
|
|
100
|
+
_wet;
|
|
101
|
+
_envelopeSmoothing;
|
|
102
|
+
constructor(options) {
|
|
103
|
+
super();
|
|
104
|
+
if (!options.modulator) {
|
|
105
|
+
throw new Error('VocoderFilter requires a modulator AudioBus.');
|
|
106
|
+
}
|
|
107
|
+
this._modulator = options.modulator;
|
|
108
|
+
this._numBands = options.numBands ?? 16;
|
|
109
|
+
this._minHz = options.minHz ?? 80;
|
|
110
|
+
this._maxHz = options.maxHz ?? 8000;
|
|
111
|
+
this._bandQ = options.bandQ ?? 4;
|
|
112
|
+
this._wet = Math.max(0, Math.min(1, options.wet ?? 1.0));
|
|
113
|
+
this._envelopeSmoothing = Math.max(0.0001, Math.min(0.1, options.envelopeSmoothing ?? 0.005));
|
|
114
|
+
}
|
|
115
|
+
get _workletName() { return 'exojs-vocoder'; }
|
|
116
|
+
get _workletSource() { return vocoderWorkletSource; }
|
|
117
|
+
get _workletOptions() {
|
|
118
|
+
return {
|
|
119
|
+
numberOfInputs: 2,
|
|
120
|
+
numberOfOutputs: 1,
|
|
121
|
+
processorOptions: {
|
|
122
|
+
numBands: this._numBands,
|
|
123
|
+
minHz: this._minHz,
|
|
124
|
+
maxHz: this._maxHz,
|
|
125
|
+
bandQ: this._bandQ,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
_onWorkletReady(audioContext) {
|
|
130
|
+
// Guard against partially-constructed instances (constructor threw after super()).
|
|
131
|
+
if (!this._modulator)
|
|
132
|
+
return;
|
|
133
|
+
this._setAudioParam('wet', this._wet);
|
|
134
|
+
this._setAudioParam('envelopeSmoothing', this._envelopeSmoothing);
|
|
135
|
+
// Wire modulator bus output to input 1 of the worklet
|
|
136
|
+
const modulator = this._modulator;
|
|
137
|
+
const modOutput = modulator._getOutputNode();
|
|
138
|
+
if (modOutput && this._workletNode) {
|
|
139
|
+
modOutput.connect(this._workletNode, 0, 1);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
modulator.onceSetup(() => {
|
|
143
|
+
const node = modulator._getOutputNode();
|
|
144
|
+
if (node && this._workletNode) {
|
|
145
|
+
node.connect(this._workletNode, 0, 1);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
get wet() { return this._wet; }
|
|
151
|
+
set wet(value) {
|
|
152
|
+
this._wet = Math.max(0, Math.min(1, value));
|
|
153
|
+
this._setAudioParam('wet', this._wet);
|
|
154
|
+
}
|
|
155
|
+
get envelopeSmoothing() { return this._envelopeSmoothing; }
|
|
156
|
+
set envelopeSmoothing(value) {
|
|
157
|
+
this._envelopeSmoothing = Math.max(0.0001, Math.min(0.1, value));
|
|
158
|
+
this._setAudioParam('envelopeSmoothing', this._envelopeSmoothing);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export { VocoderFilter };
|
|
163
|
+
//# sourceMappingURL=VocoderFilter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VocoderFilter.js","sources":["../../../../../src/audio/filters/VocoderFilter.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AudioFilter } from '../AudioFilter';
|
|
2
|
+
/**
|
|
3
|
+
* Base class for filters implemented as AudioWorkletProcessors. Subclasses
|
|
4
|
+
* declare the worklet's name, source code, and node options; this class
|
|
5
|
+
* handles the async lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* Stable input/output nodes (GainNodes) are created immediately on setup.
|
|
8
|
+
* While the worklet loads asynchronously, audio passes through directly
|
|
9
|
+
* (effectively bypassing the filter for ~10-50ms during initial load).
|
|
10
|
+
* Once the worklet loads, it's inserted into the chain and audio routes
|
|
11
|
+
* through it.
|
|
12
|
+
*
|
|
13
|
+
* Subclasses can override `_onWorkletReady` to perform additional wiring
|
|
14
|
+
* (e.g., sidechain inputs).
|
|
15
|
+
*/
|
|
16
|
+
export declare abstract class WorkletFilter extends AudioFilter {
|
|
17
|
+
protected _inputGain: GainNode | null;
|
|
18
|
+
protected _outputGain: GainNode | null;
|
|
19
|
+
protected _workletNode: AudioWorkletNode | null;
|
|
20
|
+
protected _ready: Promise<void> | null;
|
|
21
|
+
/** The processor name registered via `registerProcessor()` in the worklet source. */
|
|
22
|
+
protected abstract get _workletName(): string;
|
|
23
|
+
/** The full worklet source code as a JavaScript string. */
|
|
24
|
+
protected abstract get _workletSource(): string;
|
|
25
|
+
/** AudioWorkletNode constructor options. Default: 1 input, 1 output. */
|
|
26
|
+
protected get _workletOptions(): AudioWorkletNodeOptions;
|
|
27
|
+
constructor();
|
|
28
|
+
get inputNode(): AudioNode;
|
|
29
|
+
get outputNode(): AudioNode;
|
|
30
|
+
/**
|
|
31
|
+
* Resolves once the underlying worklet is loaded and inserted into the
|
|
32
|
+
* audio chain. Use this if you need to wait before applying parameters
|
|
33
|
+
* that depend on the worklet node.
|
|
34
|
+
*/
|
|
35
|
+
get ready(): Promise<void>;
|
|
36
|
+
destroy(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Subclass hook — called once when the worklet has loaded and the
|
|
39
|
+
* AudioWorkletNode is inserted into the chain. Use this for additional
|
|
40
|
+
* wiring (e.g., connecting a sidechain input).
|
|
41
|
+
*/
|
|
42
|
+
protected _onWorkletReady?(audioContext: AudioContext): void;
|
|
43
|
+
/** Helper for subclasses: ramp an AudioParam smoothly. */
|
|
44
|
+
protected _setAudioParam(name: string, value: number): void;
|
|
45
|
+
private _setup;
|
|
46
|
+
}
|