@blibliki/engine 0.4.2 → 0.5.1
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.d.ts +147 -129
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/Engine.ts +4 -0
- package/src/core/Note/index.ts +19 -4
- package/src/core/index.ts +7 -0
- package/src/core/midi/Message.ts +1 -0
- package/src/core/midi/MidiDeviceManager.ts +25 -0
- package/src/core/midi/deviceMatcher.ts +203 -0
- package/src/core/module/Module.ts +7 -1
- package/src/core/module/PolyModule.ts +6 -1
- package/src/core/module/VoiceScheduler.ts +9 -3
- package/src/core/module/index.ts +8 -0
- package/src/index.ts +1 -0
- package/src/modules/Inspector.ts +2 -1
- package/src/modules/MidiMapper.ts +3 -0
- package/src/modules/MidiSelector.ts +26 -5
- package/src/processors/filter-processor.ts +17 -7
- package/src/processors/scale-processor.ts +25 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blibliki/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@julusian/midi": "^3.6.1",
|
|
24
24
|
"es-toolkit": "^1.41.0",
|
|
25
|
-
"@blibliki/transport": "^0.
|
|
26
|
-
"@blibliki/utils": "^0.
|
|
25
|
+
"@blibliki/transport": "^0.5.1",
|
|
26
|
+
"@blibliki/utils": "^0.5.1"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "tsup",
|
package/src/Engine.ts
CHANGED
|
@@ -269,6 +269,10 @@ export class Engine {
|
|
|
269
269
|
return this.midiDeviceManager.findByName(name);
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
findMidiDeviceByFuzzyName(name: string, threshold?: number) {
|
|
273
|
+
return this.midiDeviceManager.findByFuzzyName(name, threshold);
|
|
274
|
+
}
|
|
275
|
+
|
|
272
276
|
onPropsUpdate(
|
|
273
277
|
callback: <T extends ModuleType>(
|
|
274
278
|
params: IModule<T> | IPolyModule<T>,
|
package/src/core/Note/index.ts
CHANGED
|
@@ -37,8 +37,15 @@ export default class Note implements INote {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
static fromEvent(message: Message) {
|
|
40
|
-
const
|
|
41
|
-
|
|
40
|
+
const dataByte = message.data[1];
|
|
41
|
+
if (dataByte === undefined) {
|
|
42
|
+
throw new Error("Invalid MIDI message: missing data byte");
|
|
43
|
+
}
|
|
44
|
+
const name = Notes[dataByte % 12];
|
|
45
|
+
if (!name) {
|
|
46
|
+
throw new Error(`Invalid MIDI note number: ${dataByte}`);
|
|
47
|
+
}
|
|
48
|
+
const octave = Math.floor(dataByte / 12) - 2;
|
|
42
49
|
|
|
43
50
|
return new Note(`${name}${octave}`);
|
|
44
51
|
}
|
|
@@ -95,9 +102,17 @@ export default class Note implements INote {
|
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
private fromString(string: string) {
|
|
98
|
-
const matches = /(\w#?)(\d)?/.exec(string)
|
|
105
|
+
const matches = /(\w#?)(\d)?/.exec(string);
|
|
106
|
+
if (!matches) {
|
|
107
|
+
throw new Error(`Invalid note string: ${string}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const name = matches[1];
|
|
111
|
+
if (!name) {
|
|
112
|
+
throw new Error(`Invalid note name in: ${string}`);
|
|
113
|
+
}
|
|
99
114
|
|
|
100
|
-
this.name =
|
|
115
|
+
this.name = name;
|
|
101
116
|
this.octave = matches[2] ? parseInt(matches[2]) : 1;
|
|
102
117
|
}
|
|
103
118
|
|
package/src/core/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type {
|
|
|
3
3
|
IModule,
|
|
4
4
|
IModuleSerialize,
|
|
5
5
|
IPolyModuleSerialize,
|
|
6
|
+
IAnyModuleSerialize,
|
|
6
7
|
SetterHooks,
|
|
7
8
|
} from "./module";
|
|
8
9
|
|
|
@@ -15,6 +16,12 @@ export { default as MidiDeviceManager } from "./midi/MidiDeviceManager";
|
|
|
15
16
|
export { default as MidiDevice, MidiPortState } from "./midi/MidiDevice";
|
|
16
17
|
export type { IMidiDevice } from "./midi/MidiDevice";
|
|
17
18
|
export { default as MidiEvent, MidiEventType } from "./midi/MidiEvent";
|
|
19
|
+
export {
|
|
20
|
+
normalizeDeviceName,
|
|
21
|
+
extractCoreTokens,
|
|
22
|
+
calculateSimilarity,
|
|
23
|
+
findBestMatch,
|
|
24
|
+
} from "./midi/deviceMatcher";
|
|
18
25
|
|
|
19
26
|
export type {
|
|
20
27
|
MidiOutput,
|
package/src/core/midi/Message.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Context } from "@blibliki/utils";
|
|
|
2
2
|
import ComputerKeyboardDevice from "./ComputerKeyboardDevice";
|
|
3
3
|
import MidiDevice from "./MidiDevice";
|
|
4
4
|
import { createMidiAdapter, type IMidiAccess } from "./adapters";
|
|
5
|
+
import { findBestMatch } from "./deviceMatcher";
|
|
5
6
|
|
|
6
7
|
type ListenerCallback = (device: MidiDevice) => void;
|
|
7
8
|
|
|
@@ -33,6 +34,30 @@ export default class MidiDeviceManager {
|
|
|
33
34
|
return Array.from(this.devices.values()).find((d) => d.name === name);
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Finds a device using fuzzy name matching
|
|
39
|
+
* Useful for matching devices across browser/Node.js environments where names differ
|
|
40
|
+
*
|
|
41
|
+
* @param targetName - The device name to match
|
|
42
|
+
* @param threshold - Minimum similarity score (0-1, default: 0.6)
|
|
43
|
+
* @returns The best matching device and confidence score, or null
|
|
44
|
+
*/
|
|
45
|
+
findByFuzzyName(
|
|
46
|
+
targetName: string,
|
|
47
|
+
threshold = 0.6,
|
|
48
|
+
): { device: MidiDevice | ComputerKeyboardDevice; score: number } | null {
|
|
49
|
+
const deviceEntries = Array.from(this.devices.values());
|
|
50
|
+
const candidateNames = deviceEntries.map((d) => d.name);
|
|
51
|
+
|
|
52
|
+
const match = findBestMatch(targetName, candidateNames, threshold);
|
|
53
|
+
|
|
54
|
+
if (!match) return null;
|
|
55
|
+
|
|
56
|
+
const device = deviceEntries.find((d) => d.name === match.name);
|
|
57
|
+
|
|
58
|
+
return device ? { device, score: match.score } : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
36
61
|
addListener(callback: ListenerCallback) {
|
|
37
62
|
this.listeners.push(callback);
|
|
38
63
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for fuzzy matching MIDI device names across different platforms
|
|
3
|
+
*
|
|
4
|
+
* MIDI devices can have different names in browser vs Node.js:
|
|
5
|
+
* - Browser: "Launchkey Mini MK3 MIDI 1"
|
|
6
|
+
* - Node.js: "Launchkey Mini MK3:Launchkey Mini MK3 MIDI 1 20:0"
|
|
7
|
+
*
|
|
8
|
+
* This module provides fuzzy matching to find the same physical device
|
|
9
|
+
* across platforms based on name similarity.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Normalizes a MIDI device name by:
|
|
14
|
+
* - Converting to lowercase
|
|
15
|
+
* - Removing common suffixes/prefixes
|
|
16
|
+
* - Removing port numbers and ALSA identifiers
|
|
17
|
+
* - Removing extra whitespace
|
|
18
|
+
* - Removing special characters
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeDeviceName(name: string): string {
|
|
21
|
+
let normalized = name.toLowerCase();
|
|
22
|
+
|
|
23
|
+
// First, remove ALSA port numbers (e.g., "20:0" at the very end)
|
|
24
|
+
// Do this BEFORE splitting by colons
|
|
25
|
+
normalized = normalized.replace(/\s+\d+:\d+\s*$/g, "");
|
|
26
|
+
|
|
27
|
+
// Remove colon-separated duplicates (Node.js format: "Device:Device Port")
|
|
28
|
+
const parts = normalized.split(":");
|
|
29
|
+
if (parts.length > 1) {
|
|
30
|
+
// Take the longest part as it usually has more info
|
|
31
|
+
normalized = parts.reduce((longest, current) =>
|
|
32
|
+
current.length > longest.length ? current : longest,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Remove common port descriptors (but keep the number if it's part of the device name)
|
|
37
|
+
// This regex only matches MIDI/Input/Output/Port at the END of the string
|
|
38
|
+
normalized = normalized.replace(
|
|
39
|
+
/\s+(midi|input|output|port)(\s+\d+)?$/gi,
|
|
40
|
+
"",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Remove "device" prefix if present at the start
|
|
44
|
+
normalized = normalized.replace(/^device\s+/gi, "");
|
|
45
|
+
|
|
46
|
+
// Remove multiple spaces
|
|
47
|
+
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
48
|
+
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extracts core device name tokens for matching
|
|
54
|
+
* Removes generic words and focuses on manufacturer/model identifiers
|
|
55
|
+
*/
|
|
56
|
+
export function extractCoreTokens(name: string): string[] {
|
|
57
|
+
const normalized = normalizeDeviceName(name);
|
|
58
|
+
|
|
59
|
+
// Split into tokens
|
|
60
|
+
const tokens = normalized.split(/[\s\-_:]+/);
|
|
61
|
+
|
|
62
|
+
// Filter out very short tokens and common generic words
|
|
63
|
+
const genericWords = new Set([
|
|
64
|
+
"midi",
|
|
65
|
+
"input",
|
|
66
|
+
"output",
|
|
67
|
+
"port",
|
|
68
|
+
"device",
|
|
69
|
+
"in",
|
|
70
|
+
"out",
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
return tokens.filter((token) => token.length > 1 && !genericWords.has(token));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Calculates Levenshtein distance between two strings
|
|
78
|
+
* Used for fuzzy string matching
|
|
79
|
+
*/
|
|
80
|
+
function levenshteinDistance(str1: string, str2: string): number {
|
|
81
|
+
const len1 = str1.length;
|
|
82
|
+
const len2 = str2.length;
|
|
83
|
+
const matrix: number[][] = [];
|
|
84
|
+
|
|
85
|
+
// Initialize matrix
|
|
86
|
+
for (let i = 0; i <= len1; i++) {
|
|
87
|
+
matrix[i] = [i];
|
|
88
|
+
}
|
|
89
|
+
for (let j = 0; j <= len2; j++) {
|
|
90
|
+
if (matrix[0]) {
|
|
91
|
+
matrix[0][j] = j;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Fill matrix
|
|
96
|
+
for (let i = 1; i <= len1; i++) {
|
|
97
|
+
for (let j = 1; j <= len2; j++) {
|
|
98
|
+
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
99
|
+
const prevRow = matrix[i - 1];
|
|
100
|
+
const currRow = matrix[i];
|
|
101
|
+
const prevCell = currRow?.[j - 1];
|
|
102
|
+
const diagCell = prevRow?.[j - 1];
|
|
103
|
+
|
|
104
|
+
const prevCellInRow = prevRow?.[j];
|
|
105
|
+
if (
|
|
106
|
+
currRow &&
|
|
107
|
+
prevCellInRow !== undefined &&
|
|
108
|
+
prevCell !== undefined &&
|
|
109
|
+
diagCell !== undefined
|
|
110
|
+
) {
|
|
111
|
+
currRow[j] = Math.min(
|
|
112
|
+
prevCellInRow + 1, // deletion
|
|
113
|
+
prevCell + 1, // insertion
|
|
114
|
+
diagCell + cost, // substitution
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lastRow = matrix[len1];
|
|
121
|
+
return lastRow?.[len2] ?? 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Calculates similarity score between two device names
|
|
126
|
+
* Returns a score between 0 (no match) and 1 (perfect match)
|
|
127
|
+
*
|
|
128
|
+
* Uses multiple strategies:
|
|
129
|
+
* 1. Exact normalized match (score: 1.0)
|
|
130
|
+
* 2. Token overlap (Jaccard similarity)
|
|
131
|
+
* 3. String similarity (based on Levenshtein distance)
|
|
132
|
+
* 4. Substring matching
|
|
133
|
+
*/
|
|
134
|
+
export function calculateSimilarity(name1: string, name2: string): number {
|
|
135
|
+
const normalized1 = normalizeDeviceName(name1);
|
|
136
|
+
const normalized2 = normalizeDeviceName(name2);
|
|
137
|
+
|
|
138
|
+
// Exact match after normalization
|
|
139
|
+
if (normalized1 === normalized2) {
|
|
140
|
+
return 1.0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const tokens1 = new Set(extractCoreTokens(name1));
|
|
144
|
+
const tokens2 = new Set(extractCoreTokens(name2));
|
|
145
|
+
|
|
146
|
+
// If no tokens extracted, fall back to pure string similarity
|
|
147
|
+
if (tokens1.size === 0 || tokens2.size === 0) {
|
|
148
|
+
const maxLen = Math.max(normalized1.length, normalized2.length);
|
|
149
|
+
if (maxLen === 0) return 0;
|
|
150
|
+
const distance = levenshteinDistance(normalized1, normalized2);
|
|
151
|
+
return 1 - distance / maxLen;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Calculate Jaccard similarity for tokens
|
|
155
|
+
const intersection = new Set([...tokens1].filter((x) => tokens2.has(x)));
|
|
156
|
+
const union = new Set([...tokens1, ...tokens2]);
|
|
157
|
+
const jaccardScore = intersection.size / union.size;
|
|
158
|
+
|
|
159
|
+
// Calculate string similarity
|
|
160
|
+
const maxLen = Math.max(normalized1.length, normalized2.length);
|
|
161
|
+
const distance = levenshteinDistance(normalized1, normalized2);
|
|
162
|
+
const stringScore = 1 - distance / maxLen;
|
|
163
|
+
|
|
164
|
+
// Check for substring matches
|
|
165
|
+
const substringScore =
|
|
166
|
+
normalized1.includes(normalized2) || normalized2.includes(normalized1)
|
|
167
|
+
? 0.8
|
|
168
|
+
: 0;
|
|
169
|
+
|
|
170
|
+
// Weighted combination of scores
|
|
171
|
+
// Token overlap is most important, then string similarity, then substring
|
|
172
|
+
const finalScore =
|
|
173
|
+
jaccardScore * 0.5 + stringScore * 0.3 + substringScore * 0.2;
|
|
174
|
+
|
|
175
|
+
return finalScore;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Finds the best matching device name from a list of candidates
|
|
180
|
+
* Returns the best match and its confidence score
|
|
181
|
+
*
|
|
182
|
+
* @param targetName - The name to match against
|
|
183
|
+
* @param candidateNames - List of possible matches
|
|
184
|
+
* @param threshold - Minimum similarity score to consider a match (default: 0.6)
|
|
185
|
+
* @returns Best match and score, or null if no match above threshold
|
|
186
|
+
*/
|
|
187
|
+
export function findBestMatch(
|
|
188
|
+
targetName: string,
|
|
189
|
+
candidateNames: string[],
|
|
190
|
+
threshold = 0.6,
|
|
191
|
+
): { name: string; score: number } | null {
|
|
192
|
+
let bestMatch: { name: string; score: number } | null = null;
|
|
193
|
+
|
|
194
|
+
for (const candidateName of candidateNames) {
|
|
195
|
+
const score = calculateSimilarity(targetName, candidateName);
|
|
196
|
+
|
|
197
|
+
if (score >= threshold && (!bestMatch || score > bestMatch.score)) {
|
|
198
|
+
bestMatch = { name: candidateName, score };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return bestMatch;
|
|
203
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ContextTime } from "@blibliki/transport";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Context,
|
|
4
|
+
Optional,
|
|
5
|
+
upperFirst,
|
|
6
|
+
uuidv4,
|
|
7
|
+
requestAnimationFrame,
|
|
8
|
+
} from "@blibliki/utils";
|
|
3
9
|
import { Engine } from "@/Engine";
|
|
4
10
|
import { AnyModule, ModuleType, ModuleTypeToPropsMapping } from "@/modules";
|
|
5
11
|
import {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { ContextTime } from "@blibliki/transport";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
deterministicId,
|
|
4
|
+
Optional,
|
|
5
|
+
uuidv4,
|
|
6
|
+
requestAnimationFrame,
|
|
7
|
+
} from "@blibliki/utils";
|
|
3
8
|
import { Engine } from "@/Engine";
|
|
4
9
|
import { ModuleType, ModuleTypeToPropsMapping } from "@/modules";
|
|
5
10
|
import {
|
|
@@ -102,9 +102,15 @@ export default class VoiceScheduler extends PolyModule<ModuleType.VoiceScheduler
|
|
|
102
102
|
let voice = this.audioModules.find((v) => !v.activeNote);
|
|
103
103
|
|
|
104
104
|
// If no available voice, get the one with the lowest triggeredAt
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
if (!voice) {
|
|
106
|
+
const sorted = this.audioModules.sort((a, b) => {
|
|
107
|
+
return a.triggeredAt - b.triggeredAt;
|
|
108
|
+
});
|
|
109
|
+
voice = sorted[0];
|
|
110
|
+
if (!voice) {
|
|
111
|
+
throw new Error("No voices available in voice scheduler");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
108
114
|
|
|
109
115
|
return voice;
|
|
110
116
|
}
|
package/src/core/module/index.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { ModuleType } from "@/modules";
|
|
2
|
+
import { IModuleSerialize } from "./Module";
|
|
3
|
+
import { IPolyModuleSerialize } from "./PolyModule";
|
|
4
|
+
|
|
1
5
|
export { Module } from "./Module";
|
|
2
6
|
export type { IModule, IModuleSerialize, SetterHooks } from "./Module";
|
|
3
7
|
export type { IPolyModule, IPolyModuleSerialize } from "./PolyModule";
|
|
8
|
+
|
|
9
|
+
export type IAnyModuleSerialize =
|
|
10
|
+
| IModuleSerialize<ModuleType>
|
|
11
|
+
| IPolyModuleSerialize<ModuleType>;
|
package/src/index.ts
CHANGED
package/src/modules/Inspector.ts
CHANGED
|
@@ -125,6 +125,7 @@ export default class MidiMapper
|
|
|
125
125
|
this.checkAutoAssign(event);
|
|
126
126
|
|
|
127
127
|
const activePage = this.props.pages[this.props.activePage];
|
|
128
|
+
if (!activePage) return;
|
|
128
129
|
|
|
129
130
|
[
|
|
130
131
|
...this.props.globalMappings.filter((m) => m.cc === event.cc),
|
|
@@ -245,6 +246,8 @@ export default class MidiMapper
|
|
|
245
246
|
if (event.cc === undefined) return;
|
|
246
247
|
|
|
247
248
|
const activePage = this.props.pages[this.props.activePage];
|
|
249
|
+
if (!activePage) return;
|
|
250
|
+
|
|
248
251
|
const hasGlobalAutoAssign = this.props.globalMappings.some(
|
|
249
252
|
({ autoAssign }) => autoAssign,
|
|
250
253
|
);
|
|
@@ -45,11 +45,32 @@ export default class MidiSelector
|
|
|
45
45
|
props,
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
// Try to find device in order of preference:
|
|
49
|
+
// 1. By exact ID match
|
|
50
|
+
// 2. By exact name match
|
|
51
|
+
// 3. By fuzzy name match (for cross-platform compatibility)
|
|
52
|
+
let midiDevice =
|
|
53
|
+
this.props.selectedId &&
|
|
54
|
+
this.engine.findMidiDevice(this.props.selectedId);
|
|
55
|
+
|
|
56
|
+
if (!midiDevice && this.props.selectedName) {
|
|
57
|
+
midiDevice = this.engine.findMidiDeviceByName(this.props.selectedName);
|
|
58
|
+
|
|
59
|
+
// If exact name match fails, try fuzzy matching
|
|
60
|
+
if (!midiDevice) {
|
|
61
|
+
const fuzzyMatch = this.engine.findMidiDeviceByFuzzyName(
|
|
62
|
+
this.props.selectedName,
|
|
63
|
+
0.6, // 60% similarity threshold
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (fuzzyMatch) {
|
|
67
|
+
midiDevice = fuzzyMatch.device;
|
|
68
|
+
console.log(
|
|
69
|
+
`MIDI device fuzzy matched: "${this.props.selectedName}" -> "${midiDevice.name}" (confidence: ${Math.round(fuzzyMatch.score * 100)}%)`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
53
74
|
|
|
54
75
|
if (midiDevice) {
|
|
55
76
|
this.addEventListener(midiDevice);
|
|
@@ -37,29 +37,39 @@ export const filterProcessorURL = URL.createObjectURL(
|
|
|
37
37
|
): boolean {
|
|
38
38
|
const input = inputs[0];
|
|
39
39
|
const output = outputs[0];
|
|
40
|
+
if (!input || !output) return true;
|
|
40
41
|
|
|
41
42
|
const cutoff = parameters.cutoff;
|
|
42
43
|
const resonance = parameters.resonance;
|
|
44
|
+
if (!cutoff || !resonance) return true;
|
|
43
45
|
|
|
44
46
|
for (let channelNum = 0; channelNum < input.length; channelNum++) {
|
|
45
47
|
const inputChannel = input[channelNum];
|
|
46
48
|
const outputChannel = output[channelNum];
|
|
49
|
+
if (!inputChannel || !outputChannel) continue;
|
|
47
50
|
|
|
48
51
|
for (let i = 0; i < inputChannel.length; i++) {
|
|
49
52
|
const s = inputChannel[i];
|
|
53
|
+
if (s === undefined) continue;
|
|
54
|
+
|
|
50
55
|
// Convert Hz to normalized frequency using logarithmic scale
|
|
51
56
|
// This better matches human hearing perception
|
|
52
|
-
const cutoffHz =
|
|
57
|
+
const cutoffHz =
|
|
58
|
+
cutoff.length > 1 ? (cutoff[i] ?? cutoff[0]) : cutoff[0];
|
|
59
|
+
if (cutoffHz === undefined) continue;
|
|
60
|
+
|
|
53
61
|
const clampedHz = Math.max(20, Math.min(20000, cutoffHz));
|
|
54
62
|
const normalizedCutoff =
|
|
55
63
|
Math.log(clampedHz / 20) / Math.log(20000 / 20);
|
|
56
64
|
const c = Math.pow(0.5, (1 - normalizedCutoff) / 0.125);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
0
|
|
61
|
-
0
|
|
62
|
-
);
|
|
65
|
+
|
|
66
|
+
const resonanceValue =
|
|
67
|
+
resonance.length > 1
|
|
68
|
+
? (resonance[i] ?? resonance[0])
|
|
69
|
+
: resonance[0];
|
|
70
|
+
if (resonanceValue === undefined) continue;
|
|
71
|
+
|
|
72
|
+
const r = Math.pow(0.5, (resonanceValue + 0.125) / 0.125);
|
|
63
73
|
const mrc = 1 - r * c;
|
|
64
74
|
|
|
65
75
|
this.s0 = mrc * this.s0 - c * this.s1 + c * s;
|
|
@@ -28,17 +28,20 @@ export const scaleProcessorURL = URL.createObjectURL(
|
|
|
28
28
|
) {
|
|
29
29
|
const input = inputs[0];
|
|
30
30
|
const output = outputs[0];
|
|
31
|
+
if (!input || !output) return true;
|
|
31
32
|
|
|
32
33
|
const minValues = parameters.min;
|
|
33
34
|
const maxValues = parameters.max;
|
|
34
35
|
const currentValues = parameters.current;
|
|
36
|
+
if (!minValues || !maxValues || !currentValues) return true;
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
const firstInput = input[0];
|
|
39
|
+
if (!firstInput || firstInput.length === 0) {
|
|
37
40
|
for (const outputChannel of output) {
|
|
38
41
|
const current =
|
|
39
|
-
|
|
40
|
-
?
|
|
41
|
-
:
|
|
42
|
+
currentValues.length > 1
|
|
43
|
+
? (currentValues[0] ?? 0.5)
|
|
44
|
+
: (currentValues[0] ?? 0.5);
|
|
42
45
|
|
|
43
46
|
outputChannel.fill(current);
|
|
44
47
|
}
|
|
@@ -49,17 +52,32 @@ export const scaleProcessorURL = URL.createObjectURL(
|
|
|
49
52
|
for (let channel = 0; channel < input.length; channel++) {
|
|
50
53
|
const inputChannel = input[channel];
|
|
51
54
|
const outputChannel = output[channel];
|
|
55
|
+
if (!inputChannel || !outputChannel) continue;
|
|
52
56
|
|
|
53
57
|
for (let i = 0; i < inputChannel.length; i++) {
|
|
54
58
|
const x = inputChannel[i];
|
|
59
|
+
if (x === undefined) continue;
|
|
55
60
|
|
|
56
|
-
const min =
|
|
57
|
-
|
|
61
|
+
const min =
|
|
62
|
+
minValues.length > 1
|
|
63
|
+
? (minValues[i] ?? minValues[0])
|
|
64
|
+
: minValues[0];
|
|
65
|
+
const max =
|
|
66
|
+
maxValues.length > 1
|
|
67
|
+
? (maxValues[i] ?? maxValues[0])
|
|
68
|
+
: maxValues[0];
|
|
58
69
|
const current =
|
|
59
70
|
currentValues.length > 1
|
|
60
|
-
? currentValues[i]
|
|
71
|
+
? (currentValues[i] ?? currentValues[0])
|
|
61
72
|
: currentValues[0];
|
|
62
73
|
|
|
74
|
+
if (
|
|
75
|
+
min === undefined ||
|
|
76
|
+
max === undefined ||
|
|
77
|
+
current === undefined
|
|
78
|
+
)
|
|
79
|
+
continue;
|
|
80
|
+
|
|
63
81
|
if (x < 0) {
|
|
64
82
|
outputChannel[i] = current * Math.pow(min / current, -x);
|
|
65
83
|
} else {
|