@blibliki/engine 0.3.5 → 0.3.7
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/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +205 -70
- package/dist/index.d.ts +205 -70
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/Engine.ts +10 -4
- package/src/core/IO/Collection.ts +4 -1
- package/src/core/index.ts +7 -2
- package/src/core/midi/MidiDevice.ts +1 -0
- package/src/core/midi/MidiEvent.ts +17 -1
- package/src/core/module/Module.ts +114 -23
- package/src/core/module/PolyModule.ts +27 -2
- package/src/core/module/VoiceScheduler.ts +3 -2
- package/src/core/module/index.ts +1 -1
- package/src/core/schema.ts +53 -8
- package/src/index.ts +10 -2
- package/src/modules/Constant.ts +10 -6
- package/src/modules/Envelope.ts +21 -38
- package/src/modules/Filter.ts +58 -39
- package/src/modules/Gain.ts +8 -6
- package/src/modules/Inspector.ts +16 -6
- package/src/modules/Master.ts +2 -3
- package/src/modules/MidiMapper.ts +292 -0
- package/src/modules/MidiSelector.ts +13 -8
- package/src/modules/Oscillator.ts +38 -21
- package/src/modules/Scale.ts +19 -10
- package/src/modules/StepSequencer.ts +2 -2
- package/src/modules/StereoPanner.ts +86 -0
- package/src/modules/VirtualMidi.ts +2 -2
- package/src/modules/index.ts +26 -15
- package/src/modules/BiquadFilter.ts +0 -162
package/src/modules/Envelope.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ContextTime } from "@blibliki/transport";
|
|
2
|
-
import { Context,
|
|
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 {
|
|
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.
|
|
19
|
-
decay: 0
|
|
20
|
-
sustain:
|
|
21
|
-
release: 0
|
|
18
|
+
attack: 0.01,
|
|
19
|
+
decay: 0,
|
|
20
|
+
sustain: 1,
|
|
21
|
+
release: 0,
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
export const envelopePropSchema:
|
|
24
|
+
export const envelopePropSchema: ModulePropSchema<IEnvelopeProps> = {
|
|
25
25
|
attack: {
|
|
26
26
|
kind: "number",
|
|
27
27
|
min: 0.0001,
|
|
28
|
-
max:
|
|
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:
|
|
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:
|
|
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.
|
|
89
|
-
const decay = this.
|
|
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
|
|
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.
|
|
114
|
+
const release = this.props.release;
|
|
122
115
|
|
|
123
116
|
// Cancel scheduled automations and set gain to the ACTUAL value at this moment
|
|
124
|
-
|
|
125
|
-
|
|
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> {
|
package/src/modules/Filter.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { Context } from "@blibliki/utils";
|
|
2
|
-
import {
|
|
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
|
-
|
|
12
|
+
type: BiquadFilterType;
|
|
13
|
+
Q: number;
|
|
15
14
|
};
|
|
16
15
|
|
|
17
16
|
const MIN_FREQ = 20;
|
|
18
|
-
const MAX_FREQ =
|
|
17
|
+
const MAX_FREQ = 20000;
|
|
19
18
|
|
|
20
19
|
const DEFAULT_PROPS: IFilterProps = {
|
|
21
20
|
cutoff: MAX_FREQ,
|
|
22
21
|
envelopeAmount: 0,
|
|
23
|
-
|
|
22
|
+
type: "lowpass",
|
|
23
|
+
Q: 1,
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
export const filterPropSchema:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
45
|
-
step: 0.
|
|
46
|
-
|
|
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
|
|
51
|
-
|
|
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
|
-
|
|
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.
|
|
106
|
+
this.scale.audioNode.connect(this.audioNode.frequency);
|
|
81
107
|
|
|
82
108
|
this.registerDefaultIOs();
|
|
83
109
|
this.registerInputs();
|
|
84
110
|
}
|
|
85
111
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
101
|
-
this.
|
|
102
|
-
}
|
|
120
|
+
onAfterSetQ: SetterHooks<IFilterProps>["onAfterSetQ"] = (value) => {
|
|
121
|
+
this.audioNode.Q.value = value;
|
|
122
|
+
};
|
|
103
123
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
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.
|
|
142
|
+
getAudioNode: () => this.audioNode.Q,
|
|
124
143
|
});
|
|
125
144
|
}
|
|
126
145
|
}
|
package/src/modules/Gain.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
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({
|
package/src/modules/Inspector.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context } from "@blibliki/utils";
|
|
2
|
-
import { IModule, Module } from "@/core";
|
|
3
|
-
import {
|
|
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:
|
|
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
|
|
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
|
-
|
|
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;
|
package/src/modules/Master.ts
CHANGED
|
@@ -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:
|
|
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
|
+
}
|