@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.
- package/README.md +22 -2
- package/dist/index.d.ts +501 -107
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/Engine.ts +46 -29
- package/src/core/index.ts +11 -2
- package/src/core/midi/BaseMidiDevice.ts +47 -0
- package/src/core/midi/ComputerKeyboardDevice.ts +2 -1
- package/src/core/midi/MidiDeviceManager.ts +125 -31
- package/src/core/midi/{MidiDevice.ts → MidiInputDevice.ts} +6 -30
- package/src/core/midi/MidiOutputDevice.ts +23 -0
- package/src/core/midi/adapters/NodeMidiAdapter.ts +99 -13
- package/src/core/midi/adapters/WebMidiAdapter.ts +68 -10
- package/src/core/midi/adapters/types.ts +13 -4
- package/src/core/midi/controllers/BaseController.ts +14 -0
- package/src/core/module/Module.ts +121 -13
- package/src/core/module/PolyModule.ts +36 -0
- package/src/core/module/VoiceScheduler.ts +150 -10
- package/src/core/module/index.ts +9 -4
- package/src/index.ts +27 -3
- package/src/modules/Chorus.ts +222 -0
- package/src/modules/Constant.ts +2 -2
- package/src/modules/Delay.ts +347 -0
- package/src/modules/Distortion.ts +182 -0
- package/src/modules/Envelope.ts +158 -92
- package/src/modules/Filter.ts +7 -7
- package/src/modules/Gain.ts +2 -2
- package/src/modules/LFO.ts +287 -0
- package/src/modules/LegacyEnvelope.ts +146 -0
- package/src/modules/{MidiSelector.ts → MidiInput.ts} +26 -19
- package/src/modules/MidiMapper.ts +59 -4
- package/src/modules/MidiOutput.ts +121 -0
- package/src/modules/Noise.ts +259 -0
- package/src/modules/Oscillator.ts +9 -3
- package/src/modules/Reverb.ts +379 -0
- package/src/modules/Scale.ts +49 -4
- package/src/modules/StepSequencer.ts +410 -22
- package/src/modules/StereoPanner.ts +1 -1
- package/src/modules/index.ts +142 -29
- package/src/processors/custom-envelope-processor.ts +125 -0
- package/src/processors/index.ts +10 -0
- package/src/processors/lfo-processor.ts +123 -0
- package/src/processors/scale-processor.ts +42 -5
- package/src/utils/WetDryMixer.ts +123 -0
- package/src/utils/expandPatternSequence.ts +18 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ContextTime } from "@blibliki/transport";
|
|
2
|
+
import { Context } from "@blibliki/utils";
|
|
3
|
+
import { GainNode } from "@blibliki/utils/web-audio-api";
|
|
4
|
+
import { Module } from "@/core";
|
|
5
|
+
import Note from "@/core/Note";
|
|
6
|
+
import { IModuleConstructor } from "@/core/module/Module";
|
|
7
|
+
import { IPolyModuleConstructor, PolyModule } from "@/core/module/PolyModule";
|
|
8
|
+
import { ModulePropSchema } from "@/core/schema";
|
|
9
|
+
import { ICreateModule, ModuleType } from ".";
|
|
10
|
+
|
|
11
|
+
export type IEnvelopeProps = {
|
|
12
|
+
attack: number;
|
|
13
|
+
decay: number;
|
|
14
|
+
sustain: number;
|
|
15
|
+
release: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const DEFAULT_PROPS: IEnvelopeProps = {
|
|
19
|
+
attack: 0.01,
|
|
20
|
+
decay: 0.1,
|
|
21
|
+
sustain: 0.7,
|
|
22
|
+
release: 0.3,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const envelopePropSchema: ModulePropSchema<IEnvelopeProps> = {
|
|
26
|
+
attack: {
|
|
27
|
+
kind: "number",
|
|
28
|
+
min: 0.001,
|
|
29
|
+
max: 10,
|
|
30
|
+
step: 0.001,
|
|
31
|
+
exp: 3,
|
|
32
|
+
label: "Attack",
|
|
33
|
+
},
|
|
34
|
+
decay: {
|
|
35
|
+
kind: "number",
|
|
36
|
+
min: 0.001,
|
|
37
|
+
max: 10,
|
|
38
|
+
step: 0.001,
|
|
39
|
+
exp: 3,
|
|
40
|
+
label: "Decay",
|
|
41
|
+
},
|
|
42
|
+
sustain: {
|
|
43
|
+
kind: "number",
|
|
44
|
+
min: 0,
|
|
45
|
+
max: 1,
|
|
46
|
+
step: 0.01,
|
|
47
|
+
label: "Sustain",
|
|
48
|
+
},
|
|
49
|
+
release: {
|
|
50
|
+
kind: "number",
|
|
51
|
+
min: 0.001,
|
|
52
|
+
max: 10,
|
|
53
|
+
step: 0.001,
|
|
54
|
+
exp: 3,
|
|
55
|
+
label: "Release",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Constants for safe audio parameter automation
|
|
60
|
+
const MIN_GAIN = 0.00001;
|
|
61
|
+
|
|
62
|
+
class MonoEnvelope extends Module<ModuleType.LegacyEnvelope> {
|
|
63
|
+
declare audioNode: GainNode;
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
engineId: string,
|
|
67
|
+
params: ICreateModule<ModuleType.LegacyEnvelope>,
|
|
68
|
+
) {
|
|
69
|
+
const props = { ...DEFAULT_PROPS, ...params.props };
|
|
70
|
+
const audioNodeConstructor = (context: Context) => {
|
|
71
|
+
const audioNode = new GainNode(context.audioContext);
|
|
72
|
+
audioNode.gain.value = 0;
|
|
73
|
+
return audioNode;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
super(engineId, {
|
|
77
|
+
...params,
|
|
78
|
+
props,
|
|
79
|
+
audioNodeConstructor,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.registerDefaultIOs();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
triggerAttack(note: Note, triggeredAt: ContextTime) {
|
|
86
|
+
super.triggerAttack(note, triggeredAt);
|
|
87
|
+
|
|
88
|
+
const { attack, decay, sustain } = this.props;
|
|
89
|
+
const gain = this.audioNode.gain;
|
|
90
|
+
|
|
91
|
+
// Exponential reset to avoid clicks when retriggering (production-style)
|
|
92
|
+
gain.cancelAndHoldAtTime(triggeredAt);
|
|
93
|
+
const resetTimeConstant = 0.002; // 2ms time constant for exponential decay
|
|
94
|
+
const resetDuration = resetTimeConstant * 5; // ~10ms total (5 time constants ≈ 99% complete)
|
|
95
|
+
gain.setTargetAtTime(MIN_GAIN, triggeredAt, resetTimeConstant);
|
|
96
|
+
|
|
97
|
+
// Attack phase: linear ramp to peak
|
|
98
|
+
const attackStartTime = triggeredAt + resetDuration;
|
|
99
|
+
const attackEndTime = attackStartTime + attack;
|
|
100
|
+
gain.setValueAtTime(MIN_GAIN, attackStartTime); // Ensure we start from MIN_GAIN
|
|
101
|
+
gain.linearRampToValueAtTime(1.0, attackEndTime);
|
|
102
|
+
|
|
103
|
+
// Decay phase: exponential ramp to sustain level
|
|
104
|
+
if (sustain < 1) {
|
|
105
|
+
const decayEndTime = attackEndTime + decay;
|
|
106
|
+
// exponentialRampToValueAtTime cannot reach 0, use MIN_GAIN instead
|
|
107
|
+
const sustainValue = sustain > 0 ? sustain : MIN_GAIN;
|
|
108
|
+
gain.exponentialRampToValueAtTime(sustainValue, decayEndTime);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
triggerRelease(note: Note, triggeredAt: ContextTime) {
|
|
113
|
+
super.triggerRelease(note, triggeredAt);
|
|
114
|
+
|
|
115
|
+
// Only release if this is the last active note
|
|
116
|
+
if (this.activeNotes.length > 0) return;
|
|
117
|
+
|
|
118
|
+
const { release } = this.props;
|
|
119
|
+
const gain = this.audioNode.gain;
|
|
120
|
+
|
|
121
|
+
// Release phase: exponential fade to silence (analog-style)
|
|
122
|
+
gain.cancelAndHoldAtTime(triggeredAt);
|
|
123
|
+
gain.setTargetAtTime(MIN_GAIN, triggeredAt, release);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default class Envelope extends PolyModule<ModuleType.LegacyEnvelope> {
|
|
128
|
+
constructor(
|
|
129
|
+
engineId: string,
|
|
130
|
+
params: IPolyModuleConstructor<ModuleType.LegacyEnvelope>,
|
|
131
|
+
) {
|
|
132
|
+
const props = { ...DEFAULT_PROPS, ...params.props };
|
|
133
|
+
const monoModuleConstructor = (
|
|
134
|
+
engineId: string,
|
|
135
|
+
params: IModuleConstructor<ModuleType.LegacyEnvelope>,
|
|
136
|
+
) => Module.create(MonoEnvelope, engineId, params);
|
|
137
|
+
|
|
138
|
+
super(engineId, {
|
|
139
|
+
...params,
|
|
140
|
+
props,
|
|
141
|
+
monoModuleConstructor,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
this.registerDefaultIOs();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
IModule,
|
|
3
|
+
Module,
|
|
4
|
+
MidiOutput,
|
|
5
|
+
SetterHooks,
|
|
6
|
+
MidiInputDevice,
|
|
7
|
+
} from "@/core";
|
|
2
8
|
import ComputerKeyboardInput from "@/core/midi/ComputerKeyboardDevice";
|
|
3
9
|
import MidiEvent from "@/core/midi/MidiEvent";
|
|
4
10
|
import { ModulePropSchema } from "@/core/schema";
|
|
5
11
|
import { ICreateModule, ModuleType } from ".";
|
|
6
12
|
|
|
7
|
-
export type
|
|
8
|
-
export type
|
|
13
|
+
export type IMidiInput = IModule<ModuleType.MidiInput>;
|
|
14
|
+
export type IMidiInputProps = {
|
|
9
15
|
selectedId: string | undefined | null;
|
|
10
16
|
selectedName: string | undefined | null;
|
|
11
17
|
};
|
|
12
18
|
|
|
13
|
-
export const
|
|
19
|
+
export const midiInputPropSchema: ModulePropSchema<IMidiInputProps> = {
|
|
14
20
|
selectedId: {
|
|
15
21
|
kind: "string",
|
|
16
22
|
label: "Midi device ID",
|
|
@@ -21,23 +27,20 @@ export const midiSelectorPropSchema: ModulePropSchema<IMidiSelectorProps> = {
|
|
|
21
27
|
},
|
|
22
28
|
};
|
|
23
29
|
|
|
24
|
-
const DEFAULT_PROPS:
|
|
30
|
+
const DEFAULT_PROPS: IMidiInputProps = {
|
|
25
31
|
selectedId: undefined,
|
|
26
32
|
selectedName: undefined,
|
|
27
33
|
};
|
|
28
34
|
|
|
29
|
-
export default class
|
|
30
|
-
extends Module<ModuleType.
|
|
31
|
-
implements Pick<SetterHooks<
|
|
35
|
+
export default class MidiInput
|
|
36
|
+
extends Module<ModuleType.MidiInput>
|
|
37
|
+
implements Pick<SetterHooks<IMidiInputProps>, "onSetSelectedId">
|
|
32
38
|
{
|
|
33
39
|
declare audioNode: undefined;
|
|
34
40
|
midiOutput!: MidiOutput;
|
|
35
41
|
_forwardMidiEvent?: (midiEvent: MidiEvent) => void;
|
|
36
42
|
|
|
37
|
-
constructor(
|
|
38
|
-
engineId: string,
|
|
39
|
-
params: ICreateModule<ModuleType.MidiSelector>,
|
|
40
|
-
) {
|
|
43
|
+
constructor(engineId: string, params: ICreateModule<ModuleType.MidiInput>) {
|
|
41
44
|
const props = { ...DEFAULT_PROPS, ...params.props };
|
|
42
45
|
|
|
43
46
|
super(engineId, {
|
|
@@ -51,14 +54,16 @@ export default class MidiSelector
|
|
|
51
54
|
// 3. By fuzzy name match (for cross-platform compatibility)
|
|
52
55
|
let midiDevice =
|
|
53
56
|
this.props.selectedId &&
|
|
54
|
-
this.engine.
|
|
57
|
+
this.engine.findMidiInputDevice(this.props.selectedId);
|
|
55
58
|
|
|
56
59
|
if (!midiDevice && this.props.selectedName) {
|
|
57
|
-
midiDevice = this.engine.
|
|
60
|
+
midiDevice = this.engine.findMidiInputDeviceByName(
|
|
61
|
+
this.props.selectedName,
|
|
62
|
+
);
|
|
58
63
|
|
|
59
64
|
// If exact name match fails, try fuzzy matching
|
|
60
65
|
if (!midiDevice) {
|
|
61
|
-
const fuzzyMatch = this.engine.
|
|
66
|
+
const fuzzyMatch = this.engine.findMidiInputDeviceByFuzzyName(
|
|
62
67
|
this.props.selectedName,
|
|
63
68
|
0.6, // 60% similarity threshold
|
|
64
69
|
);
|
|
@@ -79,13 +84,13 @@ export default class MidiSelector
|
|
|
79
84
|
this.registerOutputs();
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
onSetSelectedId: SetterHooks<
|
|
87
|
+
onSetSelectedId: SetterHooks<IMidiInputProps>["onSetSelectedId"] = (
|
|
83
88
|
value,
|
|
84
89
|
) => {
|
|
85
90
|
this.removeEventListener();
|
|
86
91
|
if (!value) return value;
|
|
87
92
|
|
|
88
|
-
const midiDevice = this.engine.
|
|
93
|
+
const midiDevice = this.engine.findMidiInputDevice(value);
|
|
89
94
|
if (!midiDevice) return value;
|
|
90
95
|
|
|
91
96
|
if (this.props.selectedName !== midiDevice.name) {
|
|
@@ -107,14 +112,16 @@ export default class MidiSelector
|
|
|
107
112
|
return this._forwardMidiEvent;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
|
-
private addEventListener(
|
|
115
|
+
private addEventListener(
|
|
116
|
+
midiDevice: MidiInputDevice | ComputerKeyboardInput,
|
|
117
|
+
) {
|
|
111
118
|
midiDevice.addEventListener(this.forwardMidiEvent);
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
private removeEventListener() {
|
|
115
122
|
if (!this.props.selectedId) return;
|
|
116
123
|
|
|
117
|
-
const midiDevice = this.engine.
|
|
124
|
+
const midiDevice = this.engine.findMidiInputDevice(this.props.selectedId);
|
|
118
125
|
midiDevice?.removeEventListener(this.forwardMidiEvent);
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ContextTime } from "@blibliki/transport";
|
|
2
|
-
import { IModule, MidiEvent, Module, SetterHooks } from "@/core";
|
|
2
|
+
import { IModule, MidiEvent, MidiOutput, Module, SetterHooks } from "@/core";
|
|
3
3
|
import { ModulePropSchema, NumberProp, PropSchema } from "@/core/schema";
|
|
4
4
|
import { ICreateModule, moduleSchemas, ModuleType } from ".";
|
|
5
5
|
|
|
@@ -26,6 +26,7 @@ export enum MidiMappingMode {
|
|
|
26
26
|
|
|
27
27
|
export type MidiMapping<T extends ModuleType> = {
|
|
28
28
|
cc?: number;
|
|
29
|
+
value?: number;
|
|
29
30
|
moduleId?: string;
|
|
30
31
|
moduleType?: T;
|
|
31
32
|
propName?: string;
|
|
@@ -102,6 +103,7 @@ export default class MidiMapper
|
|
|
102
103
|
implements MidiMapperSetterHooks
|
|
103
104
|
{
|
|
104
105
|
declare audioNode: undefined;
|
|
106
|
+
private _midiOut: MidiOutput; // Will be used to send CC values on page change
|
|
105
107
|
|
|
106
108
|
constructor(engineId: string, params: ICreateModule<ModuleType.MidiMapper>) {
|
|
107
109
|
const props = { ...DEFAULT_PROPS, ...params.props };
|
|
@@ -115,10 +117,31 @@ export default class MidiMapper
|
|
|
115
117
|
name: "midi in",
|
|
116
118
|
onMidiEvent: this.onMidiEvent,
|
|
117
119
|
});
|
|
120
|
+
|
|
121
|
+
this._midiOut = this.registerMidiOutput({
|
|
122
|
+
name: "midi out",
|
|
123
|
+
});
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
onSetActivePage: MidiMapperSetterHooks["onSetActivePage"] = (value) => {
|
|
121
|
-
|
|
127
|
+
const activePage = Math.max(
|
|
128
|
+
Math.min(value, this.props.pages.length - 1),
|
|
129
|
+
0,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const newPage = this.props.pages[activePage];
|
|
133
|
+
|
|
134
|
+
// Send stored CC values to MIDI output when changing pages
|
|
135
|
+
const now = this.context.currentTime;
|
|
136
|
+
newPage?.mappings.forEach((mapping) => {
|
|
137
|
+
if (mapping.cc !== undefined && mapping.value !== undefined) {
|
|
138
|
+
// Create CC MIDI event and send it
|
|
139
|
+
const midiEvent = MidiEvent.fromCC(mapping.cc, mapping.value, now);
|
|
140
|
+
this._midiOut.onMidiEvent(midiEvent);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return activePage;
|
|
122
145
|
};
|
|
123
146
|
|
|
124
147
|
handleCC = (event: MidiEvent, triggeredAt: ContextTime) => {
|
|
@@ -127,12 +150,44 @@ export default class MidiMapper
|
|
|
127
150
|
const activePage = this.props.pages[this.props.activePage];
|
|
128
151
|
if (!activePage) return;
|
|
129
152
|
|
|
130
|
-
[
|
|
153
|
+
const matchingMappings = [
|
|
131
154
|
...this.props.globalMappings.filter((m) => m.cc === event.cc),
|
|
132
155
|
...activePage.mappings.filter((m) => m.cc === event.cc),
|
|
133
|
-
]
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
// Forward all matching mappings
|
|
159
|
+
matchingMappings.forEach((mapping) => {
|
|
134
160
|
this.forwardMapping(event, mapping, triggeredAt);
|
|
135
161
|
});
|
|
162
|
+
|
|
163
|
+
// Update mapping values if we have matching CCs
|
|
164
|
+
if (matchingMappings.length > 0 && event.ccValue !== undefined) {
|
|
165
|
+
const updatedGlobalMappings = this.props.globalMappings.map((mapping) => {
|
|
166
|
+
if (mapping.cc === event.cc) {
|
|
167
|
+
return { ...mapping, value: event.ccValue };
|
|
168
|
+
}
|
|
169
|
+
return mapping;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const updatedPageMappings = activePage.mappings.map((mapping) => {
|
|
173
|
+
if (mapping.cc === event.cc) {
|
|
174
|
+
return { ...mapping, value: event.ccValue };
|
|
175
|
+
}
|
|
176
|
+
return mapping;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const updatedPages = this.props.pages.map((page, index) =>
|
|
180
|
+
index === this.props.activePage
|
|
181
|
+
? { ...page, mappings: updatedPageMappings }
|
|
182
|
+
: page,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
this.props = {
|
|
186
|
+
pages: updatedPages,
|
|
187
|
+
globalMappings: updatedGlobalMappings,
|
|
188
|
+
};
|
|
189
|
+
this.triggerPropsUpdate();
|
|
190
|
+
}
|
|
136
191
|
};
|
|
137
192
|
|
|
138
193
|
forwardMapping = (
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IModule,
|
|
3
|
+
Module,
|
|
4
|
+
MidiInput,
|
|
5
|
+
SetterHooks,
|
|
6
|
+
MidiOutputDevice,
|
|
7
|
+
} from "@/core";
|
|
8
|
+
import MidiEvent from "@/core/midi/MidiEvent";
|
|
9
|
+
import { ModulePropSchema } from "@/core/schema";
|
|
10
|
+
import { ICreateModule, ModuleType } from ".";
|
|
11
|
+
|
|
12
|
+
export type IMidiOutput = IModule<ModuleType.MidiOutput>;
|
|
13
|
+
export type IMidiOutputProps = {
|
|
14
|
+
selectedId: string | undefined | null;
|
|
15
|
+
selectedName: string | undefined | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const midiOutputPropSchema: ModulePropSchema<IMidiOutputProps> = {
|
|
19
|
+
selectedId: {
|
|
20
|
+
kind: "string",
|
|
21
|
+
label: "Midi device ID",
|
|
22
|
+
},
|
|
23
|
+
selectedName: {
|
|
24
|
+
kind: "string",
|
|
25
|
+
label: "Midi device name",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const DEFAULT_PROPS: IMidiOutputProps = {
|
|
30
|
+
selectedId: undefined,
|
|
31
|
+
selectedName: undefined,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default class MidiOutput
|
|
35
|
+
extends Module<ModuleType.MidiOutput>
|
|
36
|
+
implements Pick<SetterHooks<IMidiOutputProps>, "onSetSelectedId">
|
|
37
|
+
{
|
|
38
|
+
declare audioNode: undefined;
|
|
39
|
+
midiInput!: MidiInput;
|
|
40
|
+
private currentDevice?: MidiOutputDevice;
|
|
41
|
+
|
|
42
|
+
constructor(engineId: string, params: ICreateModule<ModuleType.MidiOutput>) {
|
|
43
|
+
const props = { ...DEFAULT_PROPS, ...params.props };
|
|
44
|
+
|
|
45
|
+
super(engineId, {
|
|
46
|
+
...params,
|
|
47
|
+
props,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Try to find device in order of preference:
|
|
51
|
+
// 1. By exact ID match
|
|
52
|
+
// 2. By exact name match
|
|
53
|
+
// 3. By fuzzy name match (for cross-platform compatibility)
|
|
54
|
+
let midiDevice =
|
|
55
|
+
this.props.selectedId &&
|
|
56
|
+
this.engine.findMidiOutputDevice(this.props.selectedId);
|
|
57
|
+
|
|
58
|
+
if (!midiDevice && this.props.selectedName) {
|
|
59
|
+
midiDevice = this.engine.findMidiOutputDeviceByName(
|
|
60
|
+
this.props.selectedName,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// If exact name match fails, try fuzzy matching
|
|
64
|
+
if (!midiDevice) {
|
|
65
|
+
const fuzzyMatch = this.engine.findMidiOutputDeviceByFuzzyName(
|
|
66
|
+
this.props.selectedName,
|
|
67
|
+
0.6, // 60% similarity threshold
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (fuzzyMatch) {
|
|
71
|
+
midiDevice = fuzzyMatch.device;
|
|
72
|
+
console.log(
|
|
73
|
+
`MIDI device fuzzy matched: "${this.props.selectedName}" -> "${midiDevice.name}" (confidence: ${Math.round(fuzzyMatch.score * 100)}%)`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (midiDevice) {
|
|
80
|
+
this.currentDevice = midiDevice;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.registerInputs();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onSetSelectedId: SetterHooks<IMidiOutputProps>["onSetSelectedId"] = (
|
|
87
|
+
value,
|
|
88
|
+
) => {
|
|
89
|
+
if (!value) {
|
|
90
|
+
this.currentDevice = undefined;
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const midiDevice = this.engine.findMidiOutputDevice(value);
|
|
95
|
+
if (!midiDevice) return value;
|
|
96
|
+
|
|
97
|
+
if (this.props.selectedName !== midiDevice.name) {
|
|
98
|
+
this.props = { selectedName: midiDevice.name };
|
|
99
|
+
this.triggerPropsUpdate();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.currentDevice = midiDevice;
|
|
103
|
+
|
|
104
|
+
return value;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
onMidiEvent = (midiEvent: MidiEvent) => {
|
|
108
|
+
if (!this.currentDevice) return;
|
|
109
|
+
|
|
110
|
+
// Send raw MIDI data to hardware
|
|
111
|
+
const rawData = midiEvent.rawMessage.data;
|
|
112
|
+
this.currentDevice.send(rawData);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
private registerInputs() {
|
|
116
|
+
this.midiInput = this.registerMidiInput({
|
|
117
|
+
name: "midi in",
|
|
118
|
+
onMidiEvent: this.onMidiEvent,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|