@codexo/exojs 0.6.12 → 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.
Files changed (186) hide show
  1. package/CHANGELOG.md +1316 -0
  2. package/dist/esm/audio/AbstractMedia.d.ts +18 -0
  3. package/dist/esm/audio/AbstractMedia.js +66 -0
  4. package/dist/esm/audio/AbstractMedia.js.map +1 -1
  5. package/dist/esm/audio/AudioAnalyser.d.ts +62 -23
  6. package/dist/esm/audio/AudioAnalyser.js +261 -57
  7. package/dist/esm/audio/AudioAnalyser.js.map +1 -1
  8. package/dist/esm/audio/AudioBus.d.ts +45 -0
  9. package/dist/esm/audio/AudioBus.js +219 -0
  10. package/dist/esm/audio/AudioBus.js.map +1 -0
  11. package/dist/esm/audio/AudioFilter.d.ts +9 -0
  12. package/dist/esm/audio/AudioFilter.js +7 -0
  13. package/dist/esm/audio/AudioFilter.js.map +1 -0
  14. package/dist/esm/audio/AudioListener.d.ts +20 -0
  15. package/dist/esm/audio/AudioListener.js +86 -0
  16. package/dist/esm/audio/AudioListener.js.map +1 -0
  17. package/dist/esm/audio/AudioManager.d.ts +31 -0
  18. package/dist/esm/audio/AudioManager.js +102 -0
  19. package/dist/esm/audio/AudioManager.js.map +1 -0
  20. package/dist/esm/audio/BeatDetector.d.ts +121 -0
  21. package/dist/esm/audio/BeatDetector.js +936 -0
  22. package/dist/esm/audio/BeatDetector.js.map +1 -0
  23. package/dist/esm/audio/Envelope.d.ts +44 -0
  24. package/dist/esm/audio/Envelope.js +60 -0
  25. package/dist/esm/audio/Envelope.js.map +1 -0
  26. package/dist/esm/audio/Music.d.ts +8 -0
  27. package/dist/esm/audio/Music.js +33 -4
  28. package/dist/esm/audio/Music.js.map +1 -1
  29. package/dist/esm/audio/OscillatorSound.d.ts +98 -0
  30. package/dist/esm/audio/OscillatorSound.js +342 -0
  31. package/dist/esm/audio/OscillatorSound.js.map +1 -0
  32. package/dist/esm/audio/Sound.d.ts +94 -9
  33. package/dist/esm/audio/Sound.js +283 -117
  34. package/dist/esm/audio/Sound.js.map +1 -1
  35. package/dist/esm/audio/crossFade.d.ts +19 -0
  36. package/dist/esm/audio/crossFade.js +26 -0
  37. package/dist/esm/audio/crossFade.js.map +1 -0
  38. package/dist/esm/audio/dsp/fft.d.ts +22 -0
  39. package/dist/esm/audio/dsp/mel.d.ts +43 -0
  40. package/dist/esm/audio/dsp/tempogram.d.ts +51 -0
  41. package/dist/esm/audio/filters/ChorusFilter.d.ts +47 -0
  42. package/dist/esm/audio/filters/ChorusFilter.js +139 -0
  43. package/dist/esm/audio/filters/ChorusFilter.js.map +1 -0
  44. package/dist/esm/audio/filters/CompressorFilter.d.ts +31 -0
  45. package/dist/esm/audio/filters/CompressorFilter.js +97 -0
  46. package/dist/esm/audio/filters/CompressorFilter.js.map +1 -0
  47. package/dist/esm/audio/filters/DelayFilter.d.ts +23 -0
  48. package/dist/esm/audio/filters/DelayFilter.js +100 -0
  49. package/dist/esm/audio/filters/DelayFilter.js.map +1 -0
  50. package/dist/esm/audio/filters/DuckingFilter.d.ts +31 -0
  51. package/dist/esm/audio/filters/DuckingFilter.js +152 -0
  52. package/dist/esm/audio/filters/DuckingFilter.js.map +1 -0
  53. package/dist/esm/audio/filters/EqualizerFilter.d.ts +29 -0
  54. package/dist/esm/audio/filters/EqualizerFilter.js +94 -0
  55. package/dist/esm/audio/filters/EqualizerFilter.js.map +1 -0
  56. package/dist/esm/audio/filters/GranularFilter.d.ts +56 -0
  57. package/dist/esm/audio/filters/GranularFilter.js +170 -0
  58. package/dist/esm/audio/filters/GranularFilter.js.map +1 -0
  59. package/dist/esm/audio/filters/HighpassFilter.d.ts +19 -0
  60. package/dist/esm/audio/filters/HighpassFilter.js +62 -0
  61. package/dist/esm/audio/filters/HighpassFilter.js.map +1 -0
  62. package/dist/esm/audio/filters/LowpassFilter.d.ts +19 -0
  63. package/dist/esm/audio/filters/LowpassFilter.js +62 -0
  64. package/dist/esm/audio/filters/LowpassFilter.js.map +1 -0
  65. package/dist/esm/audio/filters/PitchShiftFilter.d.ts +42 -0
  66. package/dist/esm/audio/filters/PitchShiftFilter.js +130 -0
  67. package/dist/esm/audio/filters/PitchShiftFilter.js.map +1 -0
  68. package/dist/esm/audio/filters/ReverbFilter.d.ts +24 -0
  69. package/dist/esm/audio/filters/ReverbFilter.js +107 -0
  70. package/dist/esm/audio/filters/ReverbFilter.js.map +1 -0
  71. package/dist/esm/audio/filters/VocoderFilter.d.ts +38 -0
  72. package/dist/esm/audio/filters/VocoderFilter.js +163 -0
  73. package/dist/esm/audio/filters/VocoderFilter.js.map +1 -0
  74. package/dist/esm/audio/filters/WorkletFilter.d.ts +46 -0
  75. package/dist/esm/audio/filters/WorkletFilter.js +101 -0
  76. package/dist/esm/audio/filters/WorkletFilter.js.map +1 -0
  77. package/dist/esm/audio/filters/index.d.ts +12 -0
  78. package/dist/esm/audio/index.d.ts +15 -1
  79. package/dist/esm/audio/worklet/registerWorklet.d.ts +10 -0
  80. package/dist/esm/audio/worklet/registerWorklet.js +44 -0
  81. package/dist/esm/audio/worklet/registerWorklet.js.map +1 -0
  82. package/dist/esm/core/Application.d.ts +19 -0
  83. package/dist/esm/core/Application.js +76 -2
  84. package/dist/esm/core/Application.js.map +1 -1
  85. package/dist/esm/core/SceneNode.d.ts +9 -1
  86. package/dist/esm/core/SceneNode.js +44 -6
  87. package/dist/esm/core/SceneNode.js.map +1 -1
  88. package/dist/esm/core/Time.js +1 -1
  89. package/dist/esm/core/index.d.ts +0 -1
  90. package/dist/esm/debug/BoundingBoxesLayer.d.ts +18 -0
  91. package/dist/esm/debug/BoundingBoxesLayer.js +128 -0
  92. package/dist/esm/debug/BoundingBoxesLayer.js.map +1 -0
  93. package/dist/esm/debug/DebugLayer.d.ts +29 -0
  94. package/dist/esm/debug/DebugLayer.js +26 -0
  95. package/dist/esm/debug/DebugLayer.js.map +1 -0
  96. package/dist/esm/debug/DebugOverlay.d.ts +48 -0
  97. package/dist/esm/debug/DebugOverlay.js +117 -0
  98. package/dist/esm/debug/DebugOverlay.js.map +1 -0
  99. package/dist/esm/debug/HitTestLayer.d.ts +23 -0
  100. package/dist/esm/debug/HitTestLayer.js +109 -0
  101. package/dist/esm/debug/HitTestLayer.js.map +1 -0
  102. package/dist/esm/debug/PerformanceLayer.d.ts +21 -0
  103. package/dist/esm/debug/PerformanceLayer.js +175 -0
  104. package/dist/esm/debug/PerformanceLayer.js.map +1 -0
  105. package/dist/esm/debug/PointerStackLayer.d.ts +23 -0
  106. package/dist/esm/debug/PointerStackLayer.js +152 -0
  107. package/dist/esm/debug/PointerStackLayer.js.map +1 -0
  108. package/dist/esm/debug/index.d.ts +6 -0
  109. package/dist/esm/debug/index.js +7 -0
  110. package/dist/esm/debug/index.js.map +1 -0
  111. package/dist/esm/index.js +28 -2
  112. package/dist/esm/index.js.map +1 -1
  113. package/dist/esm/input/InputManager.d.ts +10 -0
  114. package/dist/esm/input/InputManager.js +35 -5
  115. package/dist/esm/input/InputManager.js.map +1 -1
  116. package/dist/esm/input/InteractionEvent.d.ts +18 -0
  117. package/dist/esm/input/InteractionEvent.js +29 -0
  118. package/dist/esm/input/InteractionEvent.js.map +1 -0
  119. package/dist/esm/input/InteractionManager.d.ts +134 -0
  120. package/dist/esm/input/InteractionManager.js +546 -0
  121. package/dist/esm/input/InteractionManager.js.map +1 -0
  122. package/dist/esm/input/index.d.ts +2 -0
  123. package/dist/esm/input/interaction-hooks.d.ts +34 -0
  124. package/dist/esm/input/interaction-hooks.js +35 -0
  125. package/dist/esm/input/interaction-hooks.js.map +1 -0
  126. package/dist/esm/math/Circle.d.ts +12 -2
  127. package/dist/esm/math/Circle.js +82 -14
  128. package/dist/esm/math/Circle.js.map +1 -1
  129. package/dist/esm/math/Interval.js +1 -1
  130. package/dist/esm/math/ObservableVector.d.ts +2 -2
  131. package/dist/esm/math/ObservableVector.js +4 -2
  132. package/dist/esm/math/ObservableVector.js.map +1 -1
  133. package/dist/esm/math/Polygon.d.ts +15 -1
  134. package/dist/esm/math/Polygon.js +58 -6
  135. package/dist/esm/math/Polygon.js.map +1 -1
  136. package/dist/esm/math/Quadtree.d.ts +47 -0
  137. package/dist/esm/math/Quadtree.js +168 -0
  138. package/dist/esm/math/Quadtree.js.map +1 -0
  139. package/dist/esm/math/Random.js +1 -1
  140. package/dist/esm/math/Size.js +1 -1
  141. package/dist/esm/math/Vector.js +1 -1
  142. package/dist/esm/math/collision-detection.js +4 -1
  143. package/dist/esm/math/collision-detection.js.map +1 -1
  144. package/dist/esm/math/index.d.ts +1 -0
  145. package/dist/esm/particles/ParticleSystem.js +1 -0
  146. package/dist/esm/particles/ParticleSystem.js.map +1 -1
  147. package/dist/esm/particles/affectors/TorqueAffector.js +1 -1
  148. package/dist/esm/rendering/Container.d.ts +1 -0
  149. package/dist/esm/rendering/Container.js +19 -0
  150. package/dist/esm/rendering/Container.js.map +1 -1
  151. package/dist/esm/rendering/RenderNode.d.ts +27 -0
  152. package/dist/esm/rendering/RenderNode.js +44 -0
  153. package/dist/esm/rendering/RenderNode.js.map +1 -1
  154. package/dist/esm/rendering/View.d.ts +6 -4
  155. package/dist/esm/rendering/View.js +12 -2
  156. package/dist/esm/rendering/View.js.map +1 -1
  157. package/dist/esm/rendering/filters/WebGl2ShaderFilter.d.ts +109 -0
  158. package/dist/esm/rendering/filters/WebGl2ShaderFilter.js +268 -0
  159. package/dist/esm/rendering/filters/WebGl2ShaderFilter.js.map +1 -0
  160. package/dist/esm/rendering/filters/WebGpuShaderFilter.d.ts +111 -0
  161. package/dist/esm/rendering/filters/WebGpuShaderFilter.js +397 -0
  162. package/dist/esm/rendering/filters/WebGpuShaderFilter.js.map +1 -0
  163. package/dist/esm/rendering/index.d.ts +3 -0
  164. package/dist/esm/rendering/mesh/Mesh.js +1 -0
  165. package/dist/esm/rendering/mesh/Mesh.js.map +1 -1
  166. package/dist/esm/rendering/shader/upgradeFragmentShaderToGl300.d.ts +34 -0
  167. package/dist/esm/rendering/shader/upgradeFragmentShaderToGl300.js +60 -0
  168. package/dist/esm/rendering/shader/upgradeFragmentShaderToGl300.js.map +1 -0
  169. package/dist/esm/rendering/sprite/Sprite.d.ts +6 -1
  170. package/dist/esm/rendering/sprite/Sprite.js +41 -19
  171. package/dist/esm/rendering/sprite/Sprite.js.map +1 -1
  172. package/dist/esm/rendering/video/Video.d.ts +4 -0
  173. package/dist/esm/rendering/video/Video.js +32 -4
  174. package/dist/esm/rendering/video/Video.js.map +1 -1
  175. package/dist/esm/rendering/webgl2/WebGl2Backend.d.ts +4 -4
  176. package/dist/esm/rendering/webgl2/WebGl2Backend.js +7 -16
  177. package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -1
  178. package/dist/esm/rendering/webgpu/WebGpuBackend.d.ts +10 -8
  179. package/dist/esm/rendering/webgpu/WebGpuBackend.js +30 -40
  180. package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
  181. package/dist/exo.esm.js +7764 -2453
  182. package/dist/exo.esm.js.map +1 -1
  183. package/package.json +14 -2
  184. package/dist/esm/core/Quadtree.d.ts +0 -20
  185. package/dist/esm/core/Quadtree.js +0 -86
  186. 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
+ }