@blibliki/engine 0.3.4 → 0.3.6

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.
@@ -1,10 +1,10 @@
1
1
  import { ContextTime } from "@blibliki/transport";
2
- import { Context, createScaleNormalized } from "@blibliki/utils";
2
+ import { Context, cancelAndHoldAtTime } from "@blibliki/utils";
3
3
  import { Module } from "@/core";
4
4
  import Note from "@/core/Note";
5
5
  import { IModuleConstructor } from "@/core/module/Module";
6
6
  import { IPolyModuleConstructor, PolyModule } from "@/core/module/PolyModule";
7
- import { PropSchema } from "@/core/schema";
7
+ import { ModulePropSchema } from "@/core/schema";
8
8
  import { ICreateModule, ModuleType } from ".";
9
9
 
10
10
  export type IEnvelopeProps = {
@@ -15,25 +15,27 @@ export type IEnvelopeProps = {
15
15
  };
16
16
 
17
17
  const DEFAULT_PROPS: IEnvelopeProps = {
18
- attack: 0.1,
19
- decay: 0.2,
20
- sustain: 0,
21
- release: 0.3,
18
+ attack: 0.01,
19
+ decay: 0,
20
+ sustain: 1,
21
+ release: 0,
22
22
  };
23
23
 
24
- export const envelopePropSchema: PropSchema<IEnvelopeProps> = {
24
+ export const envelopePropSchema: ModulePropSchema<IEnvelopeProps> = {
25
25
  attack: {
26
26
  kind: "number",
27
27
  min: 0.0001,
28
- max: 1,
28
+ max: 20,
29
29
  step: 0.01,
30
+ exp: 3,
30
31
  label: "Attack",
31
32
  },
32
33
  decay: {
33
34
  kind: "number",
34
35
  min: 0,
35
- max: 1,
36
+ max: 20,
36
37
  step: 0.01,
38
+ exp: 3,
37
39
  label: "Decay",
38
40
  },
39
41
  sustain: {
@@ -46,22 +48,13 @@ export const envelopePropSchema: PropSchema<IEnvelopeProps> = {
46
48
  release: {
47
49
  kind: "number",
48
50
  min: 0,
49
- max: 1,
51
+ max: 20,
50
52
  step: 0.01,
53
+ exp: 3,
51
54
  label: "Release",
52
55
  },
53
56
  };
54
57
 
55
- const scaleToTen = createScaleNormalized({
56
- min: 0.001,
57
- max: 10,
58
- });
59
-
60
- const scaleToFive = createScaleNormalized({
61
- min: 0.001,
62
- max: 5,
63
- });
64
-
65
58
  class MonoEnvelope extends Module<ModuleType.Envelope> {
66
59
  declare audioNode: GainNode;
67
60
 
@@ -85,11 +78,11 @@ class MonoEnvelope extends Module<ModuleType.Envelope> {
85
78
  triggerAttack(note: Note, triggeredAt: ContextTime) {
86
79
  super.triggerAttack(note, triggeredAt);
87
80
 
88
- const attack = this.scaledAttack();
89
- const decay = this.scaledDecay();
81
+ const attack = this.props.attack;
82
+ const decay = this.props.decay;
90
83
  const sustain = this.props.sustain;
91
84
 
92
- this.audioNode.gain.cancelAndHoldAtTime(triggeredAt);
85
+ cancelAndHoldAtTime(this.audioNode.gain, triggeredAt);
93
86
 
94
87
  // Always start from a tiny value, can't ramp from 0
95
88
  if (this.audioNode.gain.value === 0) {
@@ -118,11 +111,13 @@ class MonoEnvelope extends Module<ModuleType.Envelope> {
118
111
  super.triggerRelease(note, triggeredAt);
119
112
  if (this.activeNotes.length > 0) return;
120
113
 
121
- const release = this.scaledRelease();
114
+ const release = this.props.release;
122
115
 
123
116
  // Cancel scheduled automations and set gain to the ACTUAL value at this moment
124
- this.audioNode.gain.cancelAndHoldAtTime(triggeredAt);
125
- const currentGainValue = this.audioNode.gain.value;
117
+ const currentGainValue = cancelAndHoldAtTime(
118
+ this.audioNode.gain,
119
+ triggeredAt,
120
+ );
126
121
 
127
122
  if (currentGainValue >= 0.0001) {
128
123
  // Always set the value at the release time to ensure a smooth ramp from here
@@ -137,18 +132,6 @@ class MonoEnvelope extends Module<ModuleType.Envelope> {
137
132
  // Set to zero at the very end
138
133
  this.audioNode.gain.setValueAtTime(0, triggeredAt + release);
139
134
  }
140
-
141
- private scaledAttack() {
142
- return scaleToTen(this.props.attack);
143
- }
144
-
145
- private scaledDecay() {
146
- return scaleToFive(this.props.decay);
147
- }
148
-
149
- private scaledRelease() {
150
- return scaleToTen(this.props.release);
151
- }
152
135
  }
153
136
 
154
137
  export default class Envelope extends PolyModule<ModuleType.Envelope> {
@@ -1,9 +1,7 @@
1
1
  import { Context } from "@blibliki/utils";
2
- import { Module } from "@/core";
3
- import { IModuleConstructor } from "@/core/module/Module";
2
+ import { EnumProp, ModulePropSchema } from "@/core";
3
+ import { IModuleConstructor, Module, SetterHooks } from "@/core/module/Module";
4
4
  import { IPolyModuleConstructor, PolyModule } from "@/core/module/PolyModule";
5
- import { PropSchema } from "@/core/schema";
6
- import { CustomWorklet, newAudioWorklet } from "@/processors";
7
5
  import { createModule, ICreateModule, ModuleType } from ".";
8
6
  import { MonoGain } from "./Gain";
9
7
  import Scale from "./Scale";
@@ -11,24 +9,32 @@ import Scale from "./Scale";
11
9
  export type IFilterProps = {
12
10
  cutoff: number;
13
11
  envelopeAmount: number;
14
- resonance: number;
12
+ type: BiquadFilterType;
13
+ Q: number;
15
14
  };
16
15
 
17
16
  const MIN_FREQ = 20;
18
- const MAX_FREQ = 22050;
17
+ const MAX_FREQ = 20000;
19
18
 
20
19
  const DEFAULT_PROPS: IFilterProps = {
21
20
  cutoff: MAX_FREQ,
22
21
  envelopeAmount: 0,
23
- resonance: 0,
22
+ type: "lowpass",
23
+ Q: 1,
24
24
  };
25
25
 
26
- export const filterPropSchema: PropSchema<IFilterProps> = {
26
+ export const filterPropSchema: ModulePropSchema<
27
+ IFilterProps,
28
+ {
29
+ type: EnumProp<BiquadFilterType>;
30
+ }
31
+ > = {
27
32
  cutoff: {
28
33
  kind: "number",
29
34
  min: MIN_FREQ,
30
35
  max: MAX_FREQ,
31
- step: 0.0001,
36
+ step: 1,
37
+ exp: 5,
32
38
  label: "Cutoff",
33
39
  },
34
40
  envelopeAmount: {
@@ -38,17 +44,33 @@ export const filterPropSchema: PropSchema<IFilterProps> = {
38
44
  step: 0.01,
39
45
  label: "Envelope Amount",
40
46
  },
41
- resonance: {
47
+ type: {
48
+ kind: "enum",
49
+ options: ["lowpass", "highpass", "bandpass"] satisfies BiquadFilterType[],
50
+ label: "Type",
51
+ },
52
+ Q: {
42
53
  kind: "number",
43
- min: 0,
44
- max: 4,
45
- step: 0.01,
46
- label: "resonance",
54
+ min: 0.0001,
55
+ max: 1000,
56
+ step: 0.1,
57
+ exp: 5,
58
+ label: "Q",
47
59
  },
48
60
  };
49
61
 
50
- class MonoFilter extends Module<ModuleType.Filter> {
51
- declare audioNode: AudioWorkletNode;
62
+ class MonoFilter
63
+ extends Module<ModuleType.Filter>
64
+ implements
65
+ Pick<
66
+ SetterHooks<IFilterProps>,
67
+ | "onAfterSetType"
68
+ | "onAfterSetCutoff"
69
+ | "onAfterSetQ"
70
+ | "onAfterSetEnvelopeAmount"
71
+ >
72
+ {
73
+ declare audioNode: BiquadFilterNode;
52
74
  private scale: Scale;
53
75
  private amount: MonoGain;
54
76
 
@@ -56,7 +78,11 @@ class MonoFilter extends Module<ModuleType.Filter> {
56
78
  const props = { ...DEFAULT_PROPS, ...params.props };
57
79
 
58
80
  const audioNodeConstructor = (context: Context) =>
59
- newAudioWorklet(context, CustomWorklet.FilterProcessor);
81
+ new BiquadFilterNode(context.audioContext, {
82
+ type: props.type,
83
+ frequency: 0,
84
+ Q: props.Q,
85
+ });
60
86
 
61
87
  super(engineId, {
62
88
  ...params,
@@ -77,40 +103,33 @@ class MonoFilter extends Module<ModuleType.Filter> {
77
103
  }) as Scale;
78
104
 
79
105
  this.amount.plug({ audioModule: this.scale, from: "out", to: "in" });
80
- this.scale.audioNode.connect(this.cutoff);
106
+ this.scale.audioNode.connect(this.audioNode.frequency);
81
107
 
82
108
  this.registerDefaultIOs();
83
109
  this.registerInputs();
84
110
  }
85
111
 
86
- get cutoff() {
87
- return this.audioNode.parameters.get("cutoff")!;
88
- }
89
-
90
- get resonance() {
91
- return this.audioNode.parameters.get("resonance")!;
92
- }
93
-
94
- protected onSetCutoff(value: IFilterProps["cutoff"]) {
95
- if (!this.superInitialized) return;
112
+ onAfterSetType: SetterHooks<IFilterProps>["onAfterSetType"] = (value) => {
113
+ this.audioNode.type = value;
114
+ };
96
115
 
116
+ onAfterSetCutoff: SetterHooks<IFilterProps>["onAfterSetCutoff"] = (value) => {
97
117
  this.scale.props = { current: value };
98
- }
118
+ };
99
119
 
100
- protected onSetResonance(value: IFilterProps["resonance"]) {
101
- this.resonance.value = value;
102
- }
120
+ onAfterSetQ: SetterHooks<IFilterProps>["onAfterSetQ"] = (value) => {
121
+ this.audioNode.Q.value = value;
122
+ };
103
123
 
104
- protected onSetEnvelopeAmount(value: IFilterProps["envelopeAmount"]) {
105
- if (!this.superInitialized) return;
106
-
107
- this.amount.props = { gain: value };
108
- }
124
+ onAfterSetEnvelopeAmount: SetterHooks<IFilterProps>["onAfterSetEnvelopeAmount"] =
125
+ (value) => {
126
+ this.amount.props = { gain: value };
127
+ };
109
128
 
110
129
  private registerInputs() {
111
130
  this.registerAudioInput({
112
131
  name: "cutoff",
113
- getAudioNode: () => this.scale.audioNode,
132
+ getAudioNode: () => this.audioNode.frequency,
114
133
  });
115
134
 
116
135
  this.registerAudioInput({
@@ -120,7 +139,7 @@ class MonoFilter extends Module<ModuleType.Filter> {
120
139
 
121
140
  this.registerAudioInput({
122
141
  name: "Q",
123
- getAudioNode: () => this.resonance,
142
+ getAudioNode: () => this.audioNode.Q,
124
143
  });
125
144
  }
126
145
  }
@@ -1,8 +1,7 @@
1
1
  import { Context } from "@blibliki/utils";
2
- import { IModule, Module } from "@/core";
2
+ import { IModule, Module, ModulePropSchema, SetterHooks } from "@/core";
3
3
  import { IModuleConstructor } from "@/core/module/Module";
4
4
  import { IPolyModuleConstructor, PolyModule } from "@/core/module/PolyModule";
5
- import { PropSchema } from "@/core/schema";
6
5
  import { ICreateModule, ModuleType } from ".";
7
6
 
8
7
  export type IGain = IModule<ModuleType.Gain>;
@@ -10,7 +9,7 @@ export type IGainProps = {
10
9
  gain: number;
11
10
  };
12
11
 
13
- export const gainPropSchema: PropSchema<IGainProps> = {
12
+ export const gainPropSchema: ModulePropSchema<IGainProps> = {
14
13
  gain: {
15
14
  kind: "number",
16
15
  min: 0,
@@ -22,7 +21,10 @@ export const gainPropSchema: PropSchema<IGainProps> = {
22
21
 
23
22
  const DEFAULT_PROPS: IGainProps = { gain: 1 };
24
23
 
25
- export class MonoGain extends Module<ModuleType.Gain> {
24
+ export class MonoGain
25
+ extends Module<ModuleType.Gain>
26
+ implements Pick<SetterHooks<IGainProps>, "onAfterSetGain">
27
+ {
26
28
  declare audioNode: GainNode;
27
29
 
28
30
  constructor(engineId: string, params: ICreateModule<ModuleType.Gain>) {
@@ -40,9 +42,9 @@ export class MonoGain extends Module<ModuleType.Gain> {
40
42
  this.registerAdditionalInputs();
41
43
  }
42
44
 
43
- protected onSetGain(value: IGainProps["gain"]) {
45
+ onAfterSetGain: SetterHooks<IGainProps>["onAfterSetGain"] = (value) => {
44
46
  this.audioNode.gain.value = value;
45
- }
47
+ };
46
48
 
47
49
  private registerAdditionalInputs() {
48
50
  this.registerAudioInput({
@@ -1,6 +1,6 @@
1
1
  import { Context } from "@blibliki/utils";
2
- import { IModule, Module } from "@/core";
3
- import { PropSchema } from "@/core/schema";
2
+ import { IModule, Module, SetterHooks } from "@/core";
3
+ import { EnumProp, ModulePropSchema } from "@/core/schema";
4
4
  import { ICreateModule, ModuleType } from ".";
5
5
 
6
6
  export type IInspector = IModule<ModuleType.Inspector>;
@@ -8,7 +8,12 @@ export type IInspectorProps = {
8
8
  fftSize: number;
9
9
  };
10
10
 
11
- export const inspectorPropSchema: PropSchema<IInspectorProps> = {
11
+ export const inspectorPropSchema: ModulePropSchema<
12
+ IInspectorProps,
13
+ {
14
+ fftSize: EnumProp<number>;
15
+ }
16
+ > = {
12
17
  fftSize: {
13
18
  kind: "enum",
14
19
  options: [32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768],
@@ -18,7 +23,10 @@ export const inspectorPropSchema: PropSchema<IInspectorProps> = {
18
23
 
19
24
  const DEFAULT_PROPS: IInspectorProps = { fftSize: 512 };
20
25
 
21
- export default class Inspector extends Module<ModuleType.Inspector> {
26
+ export default class Inspector
27
+ extends Module<ModuleType.Inspector>
28
+ implements Pick<SetterHooks<IInspectorProps>, "onAfterSetFftSize">
29
+ {
22
30
  declare audioNode: AnalyserNode;
23
31
  private _buffer?: Float32Array<ArrayBuffer>;
24
32
 
@@ -36,9 +44,11 @@ export default class Inspector extends Module<ModuleType.Inspector> {
36
44
  this.registerDefaultIOs("in");
37
45
  }
38
46
 
39
- protected onSetFftSize(value: number) {
47
+ onAfterSetFftSize: SetterHooks<IInspectorProps>["onAfterSetFftSize"] = (
48
+ value,
49
+ ) => {
40
50
  this._buffer = new Float32Array(value);
41
- }
51
+ };
42
52
 
43
53
  get buffer() {
44
54
  if (this._buffer) return this._buffer;
@@ -1,6 +1,5 @@
1
1
  import { Context, EmptyObject } from "@blibliki/utils";
2
- import { IModule, Module } from "@/core";
3
- import { PropSchema } from "@/core/schema";
2
+ import { IModule, Module, ModulePropSchema } from "@/core";
4
3
  import { ICreateModule, ModuleType } from ".";
5
4
 
6
5
  export type IMaster = IModule<ModuleType.Master>;
@@ -8,7 +7,7 @@ export type IMasterProps = EmptyObject;
8
7
 
9
8
  const DEFAULT_PROPS: IMasterProps = {};
10
9
 
11
- export const masterPropSchema: PropSchema<IMasterProps> = {};
10
+ export const masterPropSchema: ModulePropSchema<IMasterProps> = {};
12
11
 
13
12
  export default class Master extends Module<ModuleType.Master> {
14
13
  declare audioNode: AudioDestinationNode;
@@ -0,0 +1,292 @@
1
+ import { ContextTime } from "@blibliki/transport";
2
+ import { IModule, MidiEvent, Module, SetterHooks } from "@/core";
3
+ import { ModulePropSchema, NumberProp, PropSchema } from "@/core/schema";
4
+ import { ICreateModule, moduleSchemas, ModuleType } from ".";
5
+
6
+ export type IMidiMapper = IModule<ModuleType.MidiMapper>;
7
+ export type IMidiMapperProps = {
8
+ pages: MidiMappingPage[];
9
+ activePage: number;
10
+ globalMappings: MidiMapping<ModuleType>[];
11
+ };
12
+
13
+ export type MidiMappingPage = {
14
+ name?: string;
15
+ mappings: MidiMapping<ModuleType>[];
16
+ };
17
+
18
+ export enum MidiMappingMode {
19
+ direct = "direct",
20
+ directRev = "directRev",
21
+ toggleInc = "toggleInc",
22
+ toggleDec = "toggleDec",
23
+ incDec = "incDec",
24
+ incDecRev = "incDecRev",
25
+ }
26
+
27
+ export type MidiMapping<T extends ModuleType> = {
28
+ cc?: number;
29
+ moduleId?: string;
30
+ moduleType?: T;
31
+ propName?: string;
32
+ autoAssign?: boolean;
33
+ mode?: MidiMappingMode;
34
+ threshold?: number; // For incDec mode (default: 64)
35
+ step?: number;
36
+ };
37
+
38
+ export const midiMapperPropSchema: ModulePropSchema<IMidiMapperProps> = {
39
+ pages: {
40
+ kind: "array",
41
+ label: "Midi mapping pages",
42
+ },
43
+ activePage: {
44
+ kind: "number",
45
+ label: "Active page",
46
+ min: 0,
47
+ max: 100,
48
+ step: 1,
49
+ },
50
+ globalMappings: {
51
+ kind: "array",
52
+ label: "Global midi mappings",
53
+ },
54
+ };
55
+
56
+ const DEFAULT_PROPS: IMidiMapperProps = {
57
+ pages: [{ name: "Page 1", mappings: [{}] }],
58
+ activePage: 0,
59
+ globalMappings: [{}],
60
+ };
61
+
62
+ function getMidiFromMappedValue({
63
+ value,
64
+ midiValue,
65
+ propSchema,
66
+ mapping,
67
+ }: {
68
+ value: number;
69
+ propSchema: NumberProp;
70
+ midiValue: number;
71
+ mapping: MidiMapping<ModuleType>;
72
+ }): number {
73
+ const min = propSchema.min ?? 0;
74
+ const max = propSchema.max ?? 1;
75
+ const exp = propSchema.exp ?? 1;
76
+
77
+ const { threshold = 64, mode } = mapping;
78
+
79
+ // Reverse the range mapping: get curvedValue
80
+ const curvedValue = (value - min) / (max - min);
81
+
82
+ // Reverse the exponential curve: get normalizedMidi
83
+ const normalizedMidi = Math.pow(curvedValue, 1 / exp);
84
+
85
+ // Reverse the MIDI normalization: get midiValue
86
+ let newMidiValue = normalizedMidi * 127;
87
+ newMidiValue =
88
+ (midiValue >= threshold && mode === MidiMappingMode.incDec) ||
89
+ (midiValue <= threshold && mode === MidiMappingMode.incDecRev)
90
+ ? newMidiValue + 1
91
+ : newMidiValue - 1;
92
+ return Math.round(Math.max(0, Math.min(127, newMidiValue))); // Valid MIDI range
93
+ }
94
+
95
+ type MidiMapperSetterHooks = Pick<
96
+ SetterHooks<IMidiMapperProps>,
97
+ "onSetActivePage"
98
+ >;
99
+
100
+ export default class MidiMapper
101
+ extends Module<ModuleType.MidiMapper>
102
+ implements MidiMapperSetterHooks
103
+ {
104
+ declare audioNode: undefined;
105
+
106
+ constructor(engineId: string, params: ICreateModule<ModuleType.MidiMapper>) {
107
+ const props = { ...DEFAULT_PROPS, ...params.props };
108
+
109
+ super(engineId, {
110
+ ...params,
111
+ props,
112
+ });
113
+
114
+ this.registerMidiInput({
115
+ name: "midi in",
116
+ onMidiEvent: this.onMidiEvent,
117
+ });
118
+ }
119
+
120
+ onSetActivePage: MidiMapperSetterHooks["onSetActivePage"] = (value) => {
121
+ return Math.max(Math.min(value, this.props.pages.length - 1), 0);
122
+ };
123
+
124
+ handleCC = (event: MidiEvent, triggeredAt: ContextTime) => {
125
+ this.checkAutoAssign(event);
126
+
127
+ const activePage = this.props.pages[this.props.activePage];
128
+
129
+ [
130
+ ...this.props.globalMappings.filter((m) => m.cc === event.cc),
131
+ ...activePage.mappings.filter((m) => m.cc === event.cc),
132
+ ].forEach((mapping) => {
133
+ this.forwardMapping(event, mapping, triggeredAt);
134
+ });
135
+ };
136
+
137
+ forwardMapping = (
138
+ event: MidiEvent,
139
+ mapping: MidiMapping<ModuleType>,
140
+ _triggeredAt: ContextTime,
141
+ ) => {
142
+ if (
143
+ mapping.moduleId === undefined ||
144
+ mapping.moduleType === undefined ||
145
+ mapping.propName === undefined
146
+ )
147
+ return;
148
+
149
+ const propName = mapping.propName;
150
+ let midiValue = event.ccValue;
151
+ if (midiValue === undefined) return;
152
+
153
+ const mode = mapping.mode ?? "direct";
154
+
155
+ // Toggle mode: only respond to 127 (button press), ignore 0
156
+ if (
157
+ (mode === MidiMappingMode.toggleInc ||
158
+ mode === MidiMappingMode.toggleDec) &&
159
+ midiValue !== 127
160
+ ) {
161
+ return;
162
+ }
163
+
164
+ const mappedModule = this.engine.findModule(mapping.moduleId);
165
+ // @ts-expect-error TS7053 ignore this error
166
+ const propSchema = moduleSchemas[mappedModule.moduleType][
167
+ propName
168
+ ] as PropSchema;
169
+
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
+ let mappedValue: any;
172
+
173
+ // Direct mode (default) or Toggle mode: map value directly
174
+ switch (propSchema.kind) {
175
+ case "number": {
176
+ // @ts-expect-error TS7053 ignore this error
177
+ const currentValue = mappedModule.props[propName] as number;
178
+
179
+ if (
180
+ mode === MidiMappingMode.incDec ||
181
+ mode === MidiMappingMode.incDecRev
182
+ ) {
183
+ midiValue = getMidiFromMappedValue({
184
+ value: currentValue,
185
+ propSchema,
186
+ mapping,
187
+ midiValue,
188
+ });
189
+ } else if (mode === MidiMappingMode.directRev) {
190
+ midiValue = 127 - midiValue;
191
+ }
192
+
193
+ if (mode === MidiMappingMode.toggleInc) {
194
+ mappedValue = currentValue + (propSchema.step ?? 1);
195
+ } else if (mode === MidiMappingMode.toggleDec) {
196
+ mappedValue = currentValue - (propSchema.step ?? 1);
197
+ } else {
198
+ const min = propSchema.min ?? 0;
199
+ const max = propSchema.max ?? 1;
200
+ const normalizedMidi = midiValue / 127;
201
+ const curvedValue = Math.pow(normalizedMidi, propSchema.exp ?? 1);
202
+ mappedValue = min + curvedValue * (max - min);
203
+
204
+ // Round to step if defined
205
+ if (
206
+ propSchema.step !== undefined &&
207
+ (!propSchema.exp || propSchema.exp === 1)
208
+ ) {
209
+ const steps = Math.round((mappedValue - min) / propSchema.step);
210
+ mappedValue = min + steps * propSchema.step;
211
+ }
212
+ }
213
+
214
+ break;
215
+ }
216
+ case "enum": {
217
+ const optionIndex = Math.floor(
218
+ (midiValue / 127) * propSchema.options.length,
219
+ );
220
+ const clampedIndex = Math.min(
221
+ optionIndex,
222
+ propSchema.options.length - 1,
223
+ );
224
+ mappedValue = propSchema.options[clampedIndex];
225
+ break;
226
+ }
227
+ case "boolean":
228
+ mappedValue = midiValue >= 64;
229
+ break;
230
+ case "string":
231
+ throw Error("MidiMapper not support string type of values");
232
+ case "array":
233
+ throw Error("MidiMapper not support array type of values");
234
+
235
+ default:
236
+ throw Error("MidiMapper unknown type");
237
+ }
238
+
239
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
240
+ mappedModule.props = { [propName]: mappedValue };
241
+ mappedModule.triggerPropsUpdate();
242
+ };
243
+
244
+ private checkAutoAssign(event: MidiEvent) {
245
+ if (event.cc === undefined) return;
246
+
247
+ const activePage = this.props.pages[this.props.activePage];
248
+ const hasGlobalAutoAssign = this.props.globalMappings.some(
249
+ ({ autoAssign }) => autoAssign,
250
+ );
251
+ const hasPageAutoAssign = activePage.mappings.some(
252
+ ({ autoAssign }) => autoAssign,
253
+ );
254
+
255
+ if (!hasGlobalAutoAssign && !hasPageAutoAssign) return;
256
+
257
+ // Update global mappings if needed
258
+ const updatedGlobalMappings = hasGlobalAutoAssign
259
+ ? this.props.globalMappings.map((mapping) => {
260
+ if (!mapping.autoAssign) return mapping;
261
+
262
+ return {
263
+ ...mapping,
264
+ cc: event.cc,
265
+ autoAssign: false,
266
+ };
267
+ })
268
+ : this.props.globalMappings;
269
+
270
+ // Update page mappings if needed
271
+ const updatedPageMappings = hasPageAutoAssign
272
+ ? activePage.mappings.map((mapping) => {
273
+ if (!mapping.autoAssign) return mapping;
274
+
275
+ return {
276
+ ...mapping,
277
+ cc: event.cc,
278
+ autoAssign: false,
279
+ };
280
+ })
281
+ : activePage.mappings;
282
+
283
+ const updatedPages = this.props.pages.map((page, index) =>
284
+ index === this.props.activePage
285
+ ? { ...page, mappings: updatedPageMappings }
286
+ : page,
287
+ );
288
+
289
+ this.props = { pages: updatedPages, globalMappings: updatedGlobalMappings };
290
+ this.triggerPropsUpdate();
291
+ }
292
+ }