@blibliki/engine 0.5.2 → 0.9.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.
Files changed (47) hide show
  1. package/README.md +22 -2
  2. package/dist/index.d.ts +501 -107
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/package.json +7 -7
  6. package/src/Engine.ts +46 -29
  7. package/src/core/index.ts +11 -2
  8. package/src/core/midi/BaseMidiDevice.ts +47 -0
  9. package/src/core/midi/ComputerKeyboardDevice.ts +2 -1
  10. package/src/core/midi/MidiDeviceManager.ts +125 -31
  11. package/src/core/midi/{MidiDevice.ts → MidiInputDevice.ts} +6 -30
  12. package/src/core/midi/MidiOutputDevice.ts +23 -0
  13. package/src/core/midi/adapters/NodeMidiAdapter.ts +99 -13
  14. package/src/core/midi/adapters/WebMidiAdapter.ts +68 -10
  15. package/src/core/midi/adapters/types.ts +13 -4
  16. package/src/core/midi/controllers/BaseController.ts +14 -0
  17. package/src/core/module/Module.ts +121 -13
  18. package/src/core/module/PolyModule.ts +36 -0
  19. package/src/core/module/VoiceScheduler.ts +150 -10
  20. package/src/core/module/index.ts +9 -4
  21. package/src/index.ts +27 -3
  22. package/src/modules/Chorus.ts +222 -0
  23. package/src/modules/Constant.ts +2 -2
  24. package/src/modules/Delay.ts +347 -0
  25. package/src/modules/Distortion.ts +182 -0
  26. package/src/modules/Envelope.ts +158 -92
  27. package/src/modules/Filter.ts +7 -7
  28. package/src/modules/Gain.ts +2 -2
  29. package/src/modules/LFO.ts +287 -0
  30. package/src/modules/LegacyEnvelope.ts +146 -0
  31. package/src/modules/{MidiSelector.ts → MidiInput.ts} +26 -19
  32. package/src/modules/MidiMapper.ts +59 -4
  33. package/src/modules/MidiOutput.ts +121 -0
  34. package/src/modules/Noise.ts +259 -0
  35. package/src/modules/Oscillator.ts +9 -3
  36. package/src/modules/Reverb.ts +379 -0
  37. package/src/modules/Scale.ts +49 -4
  38. package/src/modules/StepSequencer.ts +410 -22
  39. package/src/modules/StereoPanner.ts +1 -1
  40. package/src/modules/index.ts +142 -29
  41. package/src/processors/custom-envelope-processor.ts +125 -0
  42. package/src/processors/index.ts +10 -0
  43. package/src/processors/lfo-processor.ts +123 -0
  44. package/src/processors/scale-processor.ts +42 -5
  45. package/src/utils/WetDryMixer.ts +123 -0
  46. package/src/utils/expandPatternSequence.ts +18 -0
  47. package/src/utils/index.ts +2 -0
@@ -6,6 +6,8 @@ import type {
6
6
  IMidiAccess,
7
7
  IMidiAdapter,
8
8
  IMidiInputPort,
9
+ IMidiOutputPort,
10
+ IMidiPort,
9
11
  MidiMessageCallback,
10
12
  } from "./types";
11
13
 
@@ -26,8 +28,18 @@ type NodeMidiInput = {
26
28
  isPortOpen(): boolean;
27
29
  };
28
30
 
31
+ type NodeMidiOutput = {
32
+ getPortCount(): number;
33
+ getPortName(port: number): string;
34
+ openPort(port: number): void;
35
+ closePort(): void;
36
+ sendMessage(message: number[]): void;
37
+ isPortOpen(): boolean;
38
+ };
39
+
29
40
  type NodeMidiModule = {
30
41
  Input: new () => NodeMidiInput;
42
+ Output: new () => NodeMidiOutput;
31
43
  };
32
44
 
33
45
  class NodeMidiInputPort implements IMidiInputPort {
@@ -51,8 +63,8 @@ class NodeMidiInputPort implements IMidiInputPort {
51
63
  return this._state;
52
64
  }
53
65
 
54
- setState(state: "connected" | "disconnected"): void {
55
- this._state = state;
66
+ get type() {
67
+ return "input" as const;
56
68
  }
57
69
 
58
70
  addEventListener(callback: MidiMessageCallback): void {
@@ -99,8 +111,55 @@ class NodeMidiInputPort implements IMidiInputPort {
99
111
  }
100
112
  }
101
113
 
114
+ class NodeMidiOutputPort implements IMidiOutputPort {
115
+ readonly id: string;
116
+ readonly name: string;
117
+ private portIndex: number;
118
+ private output: NodeMidiOutput;
119
+ private _state: "connected" | "disconnected" = "disconnected";
120
+ private isOpen = false;
121
+
122
+ constructor(portIndex: number, name: string, output: NodeMidiOutput) {
123
+ this.portIndex = portIndex;
124
+ this.id = `node-midi-out-${portIndex}`;
125
+ this.name = name;
126
+ this.output = output;
127
+ }
128
+
129
+ get state(): "connected" | "disconnected" {
130
+ return this._state;
131
+ }
132
+
133
+ get type() {
134
+ return "output" as const;
135
+ }
136
+
137
+ private ensureOpen(): void {
138
+ if (!this.isOpen) {
139
+ try {
140
+ this.output.openPort(this.portIndex);
141
+ this.isOpen = true;
142
+ this._state = "connected";
143
+ } catch (err) {
144
+ console.error(`Error opening MIDI output port ${this.portIndex}:`, err);
145
+ }
146
+ }
147
+ }
148
+
149
+ send(data: number[] | Uint8Array, _timestamp?: number): void {
150
+ this.ensureOpen();
151
+ try {
152
+ const message = Array.isArray(data) ? data : Array.from(data);
153
+ this.output.sendMessage(message);
154
+ } catch (err) {
155
+ console.error(`Error sending MIDI message:`, err);
156
+ }
157
+ }
158
+ }
159
+
102
160
  class NodeMidiAccess implements IMidiAccess {
103
- private ports = new Map<string, NodeMidiInputPort>();
161
+ private inputPorts = new Map<string, NodeMidiInputPort>();
162
+ private outputPorts = new Map<string, NodeMidiOutputPort>();
104
163
  private MidiModule: NodeMidiModule;
105
164
 
106
165
  constructor(MidiModule: NodeMidiModule) {
@@ -109,43 +168,70 @@ class NodeMidiAccess implements IMidiAccess {
109
168
  }
110
169
 
111
170
  private scanPorts(): void {
171
+ // Scan input ports
112
172
  try {
113
173
  const input = new this.MidiModule.Input();
114
- const portCount = input.getPortCount();
174
+ const inputCount = input.getPortCount();
115
175
 
116
- for (let i = 0; i < portCount; i++) {
176
+ for (let i = 0; i < inputCount; i++) {
117
177
  const portName = input.getPortName(i);
118
178
  const id = `node-midi-${i}`;
119
179
 
120
- if (!this.ports.has(id)) {
121
- // Create a new input instance for each port
180
+ if (!this.inputPorts.has(id)) {
122
181
  const portInput = new this.MidiModule.Input();
123
182
  const port = new NodeMidiInputPort(i, portName, portInput);
124
- this.ports.set(id, port);
183
+ this.inputPorts.set(id, port);
125
184
  }
126
185
  }
127
186
 
128
- // Clean up the scanning input
129
187
  if (input.isPortOpen()) {
130
188
  input.closePort();
131
189
  }
132
190
  } catch (err) {
133
- console.error("Error scanning MIDI ports:", err);
191
+ console.error("Error scanning MIDI input ports:", err);
192
+ }
193
+
194
+ // Scan output ports
195
+ try {
196
+ const output = new this.MidiModule.Output();
197
+ const outputCount = output.getPortCount();
198
+
199
+ for (let i = 0; i < outputCount; i++) {
200
+ const portName = output.getPortName(i);
201
+ const id = `node-midi-out-${i}`;
202
+
203
+ if (!this.outputPorts.has(id)) {
204
+ const portOutput = new this.MidiModule.Output();
205
+ const port = new NodeMidiOutputPort(i, portName, portOutput);
206
+ this.outputPorts.set(id, port);
207
+ }
208
+ }
209
+
210
+ if (output.isPortOpen()) {
211
+ output.closePort();
212
+ }
213
+ } catch (err) {
214
+ console.error("Error scanning MIDI output ports:", err);
134
215
  }
135
216
  }
136
217
 
137
218
  *inputs(): IterableIterator<IMidiInputPort> {
138
- for (const [, port] of this.ports) {
219
+ for (const [, port] of this.inputPorts) {
220
+ yield port;
221
+ }
222
+ }
223
+
224
+ *outputs(): IterableIterator<IMidiOutputPort> {
225
+ for (const [, port] of this.outputPorts) {
139
226
  yield port;
140
227
  }
141
228
  }
142
229
 
143
230
  addEventListener(
144
231
  _event: "statechange",
145
- _callback: (port: IMidiInputPort) => void,
232
+ _callback: (port: IMidiPort) => void,
146
233
  ): void {
147
234
  // node-midi doesn't support hot-plugging detection
148
- // This could be implemented with polling if needed
149
235
  console.warn(
150
236
  "Hot-plug detection not supported with node-midi adapter. Restart required for new devices.",
151
237
  );
@@ -5,6 +5,8 @@ import type {
5
5
  IMidiAccess,
6
6
  IMidiAdapter,
7
7
  IMidiInputPort,
8
+ IMidiOutputPort,
9
+ IMidiPort,
8
10
  MidiMessageCallback,
9
11
  } from "./types";
10
12
 
@@ -25,6 +27,10 @@ class WebMidiInputPort implements IMidiInputPort {
25
27
  return this.input.name ?? `Device ${this.input.id}`;
26
28
  }
27
29
 
30
+ get type() {
31
+ return this.input.type;
32
+ }
33
+
28
34
  get state(): "connected" | "disconnected" {
29
35
  return this.input.state as "connected" | "disconnected";
30
36
  }
@@ -58,9 +64,38 @@ class WebMidiInputPort implements IMidiInputPort {
58
64
  }
59
65
  }
60
66
 
67
+ class WebMidiOutputPort implements IMidiOutputPort {
68
+ private output: MIDIOutput;
69
+
70
+ constructor(output: MIDIOutput) {
71
+ this.output = output;
72
+ }
73
+
74
+ get id(): string {
75
+ return this.output.id;
76
+ }
77
+
78
+ get name(): string {
79
+ return this.output.name ?? `Device ${this.output.id}`;
80
+ }
81
+
82
+ get type() {
83
+ return this.output.type;
84
+ }
85
+
86
+ get state(): "connected" | "disconnected" {
87
+ return this.output.state as "connected" | "disconnected";
88
+ }
89
+
90
+ send(data: number[] | Uint8Array, timestamp?: number): void {
91
+ this.output.send(data, timestamp);
92
+ }
93
+ }
94
+
61
95
  class WebMidiAccess implements IMidiAccess {
62
96
  private midiAccess: MIDIAccess;
63
- private portCache = new Map<string, WebMidiInputPort>();
97
+ private inputCache = new Map<string, WebMidiInputPort>();
98
+ private outputCache = new Map<string, WebMidiOutputPort>();
64
99
 
65
100
  constructor(midiAccess: MIDIAccess) {
66
101
  this.midiAccess = midiAccess;
@@ -68,27 +103,50 @@ class WebMidiAccess implements IMidiAccess {
68
103
 
69
104
  *inputs(): IterableIterator<IMidiInputPort> {
70
105
  for (const [, input] of this.midiAccess.inputs) {
71
- if (!this.portCache.has(input.id)) {
72
- this.portCache.set(input.id, new WebMidiInputPort(input));
106
+ if (!this.inputCache.has(input.id)) {
107
+ this.inputCache.set(input.id, new WebMidiInputPort(input));
73
108
  }
74
- yield this.portCache.get(input.id)!;
109
+ yield this.inputCache.get(input.id)!;
110
+ }
111
+ }
112
+
113
+ *outputs(): IterableIterator<IMidiOutputPort> {
114
+ for (const [, output] of this.midiAccess.outputs) {
115
+ if (!this.outputCache.has(output.id)) {
116
+ this.outputCache.set(output.id, new WebMidiOutputPort(output));
117
+ }
118
+ yield this.outputCache.get(output.id)!;
75
119
  }
76
120
  }
77
121
 
78
122
  addEventListener(
79
123
  event: "statechange",
80
- callback: (port: IMidiInputPort) => void,
124
+ callback: (port: IMidiPort) => void,
81
125
  ): void {
82
126
  this.midiAccess.addEventListener(event, (e) => {
83
127
  const port = e.port;
84
- if (port?.type !== "input") return;
128
+ if (!port) return;
129
+
130
+ const midiPort: IMidiPort = {
131
+ id: port.id,
132
+ name: port.name ?? `Device ${port.id}`,
133
+ state: port.state as "connected" | "disconnected",
134
+ type: port.type as "input" | "output",
135
+ };
85
136
 
86
- const input = port as MIDIInput;
87
- if (!this.portCache.has(input.id)) {
88
- this.portCache.set(input.id, new WebMidiInputPort(input));
137
+ if (port.type === "input") {
138
+ const input = port as MIDIInput;
139
+ if (!this.inputCache.has(input.id)) {
140
+ this.inputCache.set(input.id, new WebMidiInputPort(input));
141
+ }
142
+ } else {
143
+ const output = port as MIDIOutput;
144
+ if (!this.outputCache.has(output.id)) {
145
+ this.outputCache.set(output.id, new WebMidiOutputPort(output));
146
+ }
89
147
  }
90
148
 
91
- callback(this.portCache.get(input.id)!);
149
+ callback(midiPort);
92
150
  });
93
151
  }
94
152
  }
@@ -10,19 +10,28 @@ export interface IMidiMessageEvent {
10
10
 
11
11
  export type MidiMessageCallback = (event: IMidiMessageEvent) => void;
12
12
 
13
- export interface IMidiInputPort {
13
+ export interface IMidiInputPort extends IMidiPort {
14
+ addEventListener(callback: MidiMessageCallback): void;
15
+ removeEventListener(callback: MidiMessageCallback): void;
16
+ }
17
+
18
+ export interface IMidiOutputPort extends IMidiPort {
19
+ send(data: number[] | Uint8Array, timestamp?: number): void;
20
+ }
21
+
22
+ export interface IMidiPort {
14
23
  readonly id: string;
15
24
  readonly name: string;
16
25
  readonly state: "connected" | "disconnected";
17
- addEventListener(callback: MidiMessageCallback): void;
18
- removeEventListener(callback: MidiMessageCallback): void;
26
+ readonly type: "input" | "output";
19
27
  }
20
28
 
21
29
  export interface IMidiAccess {
22
30
  inputs(): IterableIterator<IMidiInputPort>;
31
+ outputs(): IterableIterator<IMidiOutputPort>;
23
32
  addEventListener(
24
33
  event: "statechange",
25
- callback: (port: IMidiInputPort) => void,
34
+ callback: (port: IMidiPort) => void,
26
35
  ): void;
27
36
  }
28
37
 
@@ -0,0 +1,14 @@
1
+ import type { IMidiOutputPort } from "../adapters";
2
+
3
+ export abstract class BaseController {
4
+ protected outputPort: IMidiOutputPort;
5
+ protected isInDawMode = false;
6
+
7
+ constructor(outputPort: IMidiOutputPort) {
8
+ this.outputPort = outputPort;
9
+ }
10
+
11
+ abstract enterDawMode(): Promise<void>;
12
+
13
+ abstract exitDawMode(): Promise<void>;
14
+ }
@@ -7,7 +7,13 @@ import {
7
7
  requestAnimationFrame,
8
8
  } from "@blibliki/utils";
9
9
  import { Engine } from "@/Engine";
10
- import { AnyModule, ModuleType, ModuleTypeToPropsMapping } from "@/modules";
10
+ import {
11
+ AnyModule,
12
+ ICreateModule,
13
+ ModuleType,
14
+ ModuleTypeToPropsMapping,
15
+ ModuleTypeToStateMapping,
16
+ } from "@/modules";
11
17
  import {
12
18
  AudioInputProps,
13
19
  AudioOutputProps,
@@ -49,6 +55,16 @@ export type SetterHooks<P> = {
49
55
  ) => void;
50
56
  };
51
57
 
58
+ export type StateSetterHooks<S> = {
59
+ [K in keyof S as `onSetState${Capitalize<string & K>}`]: (
60
+ value: S[K],
61
+ ) => S[K];
62
+ } & {
63
+ [K in keyof S as `onAfterSetState${Capitalize<string & K>}`]: (
64
+ value: S[K],
65
+ ) => void;
66
+ };
67
+
52
68
  export abstract class Module<T extends ModuleType> implements IModule<T> {
53
69
  id: string;
54
70
  engineId: string;
@@ -59,9 +75,46 @@ export abstract class Module<T extends ModuleType> implements IModule<T> {
59
75
  inputs: InputCollection;
60
76
  outputs: OutputCollection;
61
77
  protected _props!: ModuleTypeToPropsMapping[T];
78
+ protected _state!: ModuleTypeToStateMapping[T];
62
79
  protected activeNotes: Note[];
80
+ protected _propsInitialized = false;
63
81
  private pendingUIUpdates = false;
64
82
 
83
+ /**
84
+ * Factory method for creating modules with proper initialization timing.
85
+ *
86
+ * This method ensures hooks are called AFTER the child class constructor completes,
87
+ * solving the ES6 class field initialization problem where function properties like hooks
88
+ * aren't available during super() call.
89
+ *
90
+ * @example
91
+ * const gain = Module.create(MonoGain, engineId, {
92
+ * name: "gain",
93
+ * moduleType: ModuleType.Gain,
94
+ * props: { gain: 0.5 }
95
+ * });
96
+ */
97
+ static create<T extends ModuleType, M extends Module<T>>(
98
+ ModuleClass: new (engineId: string, params: ICreateModule<T>) => M,
99
+ engineId: string,
100
+ params: Omit<IModuleConstructor<T>, "props"> & {
101
+ props: Partial<IModule<T>["props"]>;
102
+ },
103
+ ): M {
104
+ // Create instance with deferred prop initialization
105
+ const instance = new ModuleClass(engineId, {
106
+ ...params,
107
+ });
108
+
109
+ // Now trigger prop setters after child constructor has completed
110
+ // At this point, all child class properties (including arrow functions) exist
111
+ // TODO: We have to refactor all modules the remove the props assignment from constructor
112
+ instance.props = { ...instance.props };
113
+ instance._propsInitialized = true;
114
+
115
+ return instance;
116
+ }
117
+
65
118
  constructor(engineId: string, params: IModuleConstructor<T>) {
66
119
  const { id, name, moduleType, voiceNo, audioNodeConstructor, props } =
67
120
  params;
@@ -77,11 +130,6 @@ export abstract class Module<T extends ModuleType> implements IModule<T> {
77
130
 
78
131
  this.inputs = new InputCollection(this);
79
132
  this.outputs = new OutputCollection(this);
80
-
81
- // Defer hook calls until after subclass is fully initialized
82
- queueMicrotask(() => {
83
- this.props = props;
84
- });
85
133
  }
86
134
 
87
135
  get props(): ModuleTypeToPropsMapping[T] {
@@ -89,20 +137,25 @@ export abstract class Module<T extends ModuleType> implements IModule<T> {
89
137
  }
90
138
 
91
139
  set props(value: Partial<ModuleTypeToPropsMapping[T]>) {
92
- const updatedValue = { ...value };
140
+ const updatedValue: Partial<ModuleTypeToPropsMapping[T]> = {};
141
+ const isFirstSet = !this._propsInitialized;
93
142
 
94
143
  (Object.keys(value) as (keyof ModuleTypeToPropsMapping[T])[]).forEach(
95
144
  (key) => {
96
145
  const propValue = value[key];
97
- if (propValue !== undefined) {
146
+ // On first set, always include the value. On subsequent sets, only if it changed.
147
+ if (
148
+ propValue !== undefined &&
149
+ (isFirstSet || this._props[key] !== propValue)
150
+ ) {
98
151
  const result = this.callPropHook("onSet", key, propValue);
99
- if (result !== undefined) {
100
- updatedValue[key] = result;
101
- }
152
+ updatedValue[key] = result ?? propValue;
102
153
  }
103
154
  },
104
155
  );
105
156
 
157
+ if (Object.keys(updatedValue).length === 0) return;
158
+
106
159
  this._props = { ...this._props, ...updatedValue };
107
160
 
108
161
  (
@@ -134,6 +187,56 @@ export abstract class Module<T extends ModuleType> implements IModule<T> {
134
187
  return undefined;
135
188
  }
136
189
 
190
+ get state(): ModuleTypeToStateMapping[T] {
191
+ return this._state;
192
+ }
193
+
194
+ set state(value: Partial<ModuleTypeToStateMapping[T]>) {
195
+ const updatedValue: Partial<ModuleTypeToStateMapping[T]> = {};
196
+
197
+ (Object.keys(value) as (keyof ModuleTypeToStateMapping[T])[]).forEach(
198
+ (key) => {
199
+ const stateValue = value[key];
200
+ if (stateValue !== undefined && this._state[key] !== stateValue) {
201
+ const result = this.callStateHook("onSetState", key, stateValue);
202
+ updatedValue[key] = result ?? stateValue;
203
+ }
204
+ },
205
+ );
206
+
207
+ if (Object.keys(updatedValue).length === 0) return;
208
+
209
+ this._state = { ...this._state, ...updatedValue };
210
+
211
+ (
212
+ Object.keys(updatedValue) as (keyof ModuleTypeToStateMapping[T])[]
213
+ ).forEach((key) => {
214
+ const stateValue = updatedValue[key];
215
+ if (stateValue !== undefined) {
216
+ this.callStateHook("onAfterSetState", key, stateValue);
217
+ }
218
+ });
219
+ }
220
+
221
+ private callStateHook<K extends keyof ModuleTypeToStateMapping[T]>(
222
+ hookType: "onSetState" | "onAfterSetState",
223
+ key: K,
224
+ value: ModuleTypeToStateMapping[T][K],
225
+ ): ModuleTypeToStateMapping[T][K] | undefined {
226
+ const hookName = `${hookType}${upperFirst(key as string)}`;
227
+ const hook = this[hookName as keyof this];
228
+
229
+ if (typeof hook === "function") {
230
+ const result = (
231
+ hook as (
232
+ value: ModuleTypeToStateMapping[T][K],
233
+ ) => ModuleTypeToStateMapping[T][K] | undefined
234
+ ).call(this, value);
235
+ return result;
236
+ }
237
+ return undefined;
238
+ }
239
+
137
240
  serialize(): IModuleSerialize<T> {
138
241
  return {
139
242
  id: this.id,
@@ -223,13 +326,18 @@ export abstract class Module<T extends ModuleType> implements IModule<T> {
223
326
 
224
327
  private sheduleTriggerUpdate() {
225
328
  requestAnimationFrame(() => {
226
- this.engine._triggerPropsUpdate({
329
+ const updateParams: IModule<T> & {
330
+ state?: ModuleTypeToStateMapping[T];
331
+ } = {
227
332
  id: this.id,
228
333
  moduleType: this.moduleType,
229
334
  voiceNo: this.voiceNo,
230
335
  name: this.name,
231
336
  props: this.props,
232
- });
337
+ state: this._state,
338
+ };
339
+
340
+ this.engine._triggerPropsUpdate(updateParams);
233
341
  this.pendingUIUpdates = false;
234
342
  });
235
343
  }
@@ -53,6 +53,35 @@ export abstract class PolyModule<
53
53
  private _name!: string;
54
54
  private pendingUIUpdates = false;
55
55
 
56
+ /**
57
+ * Factory method for creating modules with proper initialization timing.
58
+ *
59
+ * This method ensures hooks are called AFTER the child class constructor completes,
60
+ * solving the ES6 class field initialization problem where function properties like hooks
61
+ * aren't available during super() call.
62
+ *
63
+ * @example
64
+ * const gain = Module.create(Gain, engineId, {
65
+ * name: "gain",
66
+ * moduleType: ModuleType.Gain,
67
+ * props: { gain: 0.5 }
68
+ * });
69
+ */
70
+ static create<T extends ModuleType, M extends PolyModule<T>>(
71
+ ModuleClass: new (engineId: string, params: IPolyModuleConstructor<T>) => M,
72
+ engineId: string,
73
+ params: IPolyModuleConstructor<T>,
74
+ ): M {
75
+ // Create instance with deferred prop initialization
76
+ const instance = new ModuleClass(engineId, {
77
+ ...params,
78
+ });
79
+
80
+ instance.props = { ...instance.props };
81
+
82
+ return instance;
83
+ }
84
+
56
85
  constructor(engineId: string, params: IPolyModuleConstructor<T>) {
57
86
  const { id, name, moduleType, voices, monoModuleConstructor, props } =
58
87
  params;
@@ -167,6 +196,13 @@ export abstract class PolyModule<
167
196
  }
168
197
 
169
198
  onMidiEvent = (midiEvent: MidiEvent) => {
199
+ if (midiEvent.cc) {
200
+ this.audioModules.forEach((m) => {
201
+ m.onMidiEvent(midiEvent);
202
+ });
203
+ return;
204
+ }
205
+
170
206
  const voiceNo = midiEvent.voiceNo ?? 0;
171
207
  const audioModule = this.findVoice(voiceNo);
172
208
  audioModule.onMidiEvent(midiEvent);