@vessel-dsp/core 0.5.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 (99) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +6 -0
  3. package/package.json +56 -0
  4. package/src/editor/commands.ts +344 -0
  5. package/src/editor/factory.ts +148 -0
  6. package/src/editor/history.ts +142 -0
  7. package/src/editor/index.ts +11 -0
  8. package/src/editor/layout.ts +207 -0
  9. package/src/formats/circuit-json/serializer.ts +1410 -0
  10. package/src/formats/document.ts +274 -0
  11. package/src/formats/interchange/parser.ts +1165 -0
  12. package/src/formats/interchange/serializer.ts +594 -0
  13. package/src/formats/ltspice/catalog.ts +181 -0
  14. package/src/formats/ltspice/encoding.ts +151 -0
  15. package/src/formats/ltspice/parser.ts +432 -0
  16. package/src/formats/ltspice/serializer.ts +169 -0
  17. package/src/formats/schx/catalog.ts +439 -0
  18. package/src/formats/schx/parser.ts +261 -0
  19. package/src/formats/schx/runtime-descriptors.ts +502 -0
  20. package/src/formats/schx/serializer.ts +211 -0
  21. package/src/formats/schx/transforms.ts +38 -0
  22. package/src/formats/spice/parser.ts +373 -0
  23. package/src/formats/spice/serializer.ts +43 -0
  24. package/src/index.ts +205 -0
  25. package/src/model/connectivity.ts +239 -0
  26. package/src/model/netlist.ts +375 -0
  27. package/src/model/properties.ts +101 -0
  28. package/src/model/quantity.ts +173 -0
  29. package/src/model/types.ts +309 -0
  30. package/src/model/validation.ts +985 -0
  31. package/src/model/wires.ts +86 -0
  32. package/src/panel/extract.ts +878 -0
  33. package/src/panel/index.ts +39 -0
  34. package/src/panel/knobs.ts +70 -0
  35. package/src/panel/protocol.ts +117 -0
  36. package/src/panel/types.ts +180 -0
  37. package/src/preview/bounds.ts +85 -0
  38. package/src/preview/box-layout.ts +24 -0
  39. package/src/preview/colors.ts +43 -0
  40. package/src/preview/hanging.ts +94 -0
  41. package/src/preview/junctions.ts +94 -0
  42. package/src/preview/label-layout.ts +90 -0
  43. package/src/preview/ports.ts +101 -0
  44. package/src/preview/renderable-wires.ts +113 -0
  45. package/src/preview/routing.ts +15 -0
  46. package/src/preview/snap.ts +104 -0
  47. package/src/preview/symbols/analog-switch.svg +17 -0
  48. package/src/preview/symbols/battery.svg +16 -0
  49. package/src/preview/symbols/bbd.svg +21 -0
  50. package/src/preview/symbols/bjt-npn.svg +16 -0
  51. package/src/preview/symbols/bjt-pnp.svg +17 -0
  52. package/src/preview/symbols/capacitor-electrolytic.svg +13 -0
  53. package/src/preview/symbols/capacitor.svg +12 -0
  54. package/src/preview/symbols/current-source.svg +14 -0
  55. package/src/preview/symbols/delay-ic.svg +22 -0
  56. package/src/preview/symbols/diode-schottky.svg +12 -0
  57. package/src/preview/symbols/diode-zener.svg +12 -0
  58. package/src/preview/symbols/diode.svg +13 -0
  59. package/src/preview/symbols/flipflop.svg +20 -0
  60. package/src/preview/symbols/ground.svg +12 -0
  61. package/src/preview/symbols/ic-block.svg +20 -0
  62. package/src/preview/symbols/ic.svg +19 -0
  63. package/src/preview/symbols/inductor.svg +11 -0
  64. package/src/preview/symbols/jack-input.svg +16 -0
  65. package/src/preview/symbols/jack-output.svg +16 -0
  66. package/src/preview/symbols/jfet-junction-n.svg +17 -0
  67. package/src/preview/symbols/jfet-n.svg +17 -0
  68. package/src/preview/symbols/jfet-p.svg +17 -0
  69. package/src/preview/symbols/label.svg +8 -0
  70. package/src/preview/symbols/led.svg +18 -0
  71. package/src/preview/symbols/mosfet-n.svg +21 -0
  72. package/src/preview/symbols/mosfet-p.svg +21 -0
  73. package/src/preview/symbols/named-wire.svg +11 -0
  74. package/src/preview/symbols/opamp.svg +21 -0
  75. package/src/preview/symbols/optocoupler.svg +30 -0
  76. package/src/preview/symbols/ota.svg +20 -0
  77. package/src/preview/symbols/pentode.svg +25 -0
  78. package/src/preview/symbols/photoresistor.svg +19 -0
  79. package/src/preview/symbols/port.svg +8 -0
  80. package/src/preview/symbols/potentiometer.svg +15 -0
  81. package/src/preview/symbols/power-amp.svg +20 -0
  82. package/src/preview/symbols/rail.svg +11 -0
  83. package/src/preview/symbols/regulator.svg +13 -0
  84. package/src/preview/symbols/relay.svg +20 -0
  85. package/src/preview/symbols/resistor.svg +11 -0
  86. package/src/preview/symbols/svg-content.ts +59 -0
  87. package/src/preview/symbols/switch-3pdt.svg +32 -0
  88. package/src/preview/symbols/switch-rotary.svg +23 -0
  89. package/src/preview/symbols/switch-spdt.svg +16 -0
  90. package/src/preview/symbols/switch-spst.svg +14 -0
  91. package/src/preview/symbols/switch-toggle.svg +14 -0
  92. package/src/preview/symbols/transformer.svg +17 -0
  93. package/src/preview/symbols/triode.svg +17 -0
  94. package/src/preview/symbols/tube-diode.svg +13 -0
  95. package/src/preview/symbols/unsupported.svg +8 -0
  96. package/src/preview/symbols/variable-resistor.svg +13 -0
  97. package/src/preview/symbols/voltage-source.svg +15 -0
  98. package/src/preview/symbols.ts +207 -0
  99. package/src/preview/wire-chains.ts +153 -0
@@ -0,0 +1,502 @@
1
+ import {
2
+ propertyBooleanValue,
3
+ propertyNumericValue,
4
+ propertyStringValue,
5
+ } from '../../model/properties';
6
+ import type { PropertyValue } from '../../model/types';
7
+
8
+ type MutableProperties = Record<string, PropertyValue>;
9
+
10
+ export function runtimeDescriptorProperties(
11
+ shortType: string,
12
+ properties: Readonly<Record<string, PropertyValue>>,
13
+ ): Readonly<Record<string, PropertyValue>> {
14
+ const descriptorType = descriptorTypeForShortType(shortType);
15
+ const out: MutableProperties = {
16
+ DescriptorType: descriptorType,
17
+ };
18
+
19
+ switch (descriptorType) {
20
+ case 'microblock-tone-stack':
21
+ Object.assign(out, toneStackDescriptor(properties));
22
+ break;
23
+ case 'microblock-active-eq':
24
+ Object.assign(out, activeEqDescriptor(properties));
25
+ break;
26
+ case 'microblock-delay-chip':
27
+ Object.assign(out, delayChipDescriptor(properties));
28
+ break;
29
+ case 'microblock-reverb':
30
+ Object.assign(out, reverbDescriptor(properties));
31
+ break;
32
+ case 'microblock-compressor':
33
+ Object.assign(out, compressorDescriptor(properties));
34
+ break;
35
+ case 'microblock-octave':
36
+ Object.assign(out, octaveDescriptor(properties));
37
+ break;
38
+ case 'microblock-envelope-gain':
39
+ case 'microblock-env-filter':
40
+ Object.assign(out, scalarDescriptor(properties, COMMON_RUNTIME_FIELDS));
41
+ break;
42
+ default:
43
+ break;
44
+ }
45
+
46
+ return out;
47
+ }
48
+
49
+ function descriptorTypeForShortType(shortType: string): string {
50
+ if (shortType.startsWith('MicroBlock')) {
51
+ const rest = shortType.slice('MicroBlock'.length);
52
+ return rest.length === 0 ? 'microblock' : `microblock-${camelToKebab(rest)}`;
53
+ }
54
+ return camelToKebab(shortType);
55
+ }
56
+
57
+ function camelToKebab(value: string): string {
58
+ return value
59
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
60
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
61
+ .toLowerCase();
62
+ }
63
+
64
+ function toneStackDescriptor(properties: Readonly<Record<string, PropertyValue>>): MutableProperties {
65
+ const out: MutableProperties = {};
66
+ addNumber(out, 'overallGainDb', properties, ['OverallGainDb']);
67
+ addNumber(out, 'maxSections', properties, ['MaxSections']);
68
+ const sections = parseToneStackSections(properties);
69
+ if (sections.length > 0) {
70
+ out.sections = sections;
71
+ }
72
+ addString(out, 'toneControlName', properties, ['ToneControl', 'ToneControlName']);
73
+ addNumber(out, 'minToneWipe', properties, ['MinToneWipe']);
74
+ addNumber(out, 'maxToneWipe', properties, ['MaxToneWipe']);
75
+ addNumber(out, 'defaultToneWipe', properties, ['DefaultToneWipe', 'ToneControlWipe']);
76
+ addNumber(out, 'defaultBassWipe', properties, ['DefaultBassWipe', 'BassControlWipe']);
77
+ addNumber(out, 'defaultMiddleWipe', properties, ['DefaultMiddleWipe', 'MiddleControlWipe']);
78
+ addNumber(out, 'defaultTrebleWipe', properties, ['DefaultTrebleWipe', 'TrebleControlWipe']);
79
+ return out;
80
+ }
81
+
82
+ function activeEqDescriptor(properties: Readonly<Record<string, PropertyValue>>): MutableProperties {
83
+ const out = scalarDescriptor(properties, [
84
+ ['inputGain', ['InputGain']],
85
+ ['outputGain', ['OutputGain']],
86
+ ['minInputGain', ['MinInputGain']],
87
+ ['maxInputGain', ['MaxInputGain']],
88
+ ['mix', ['Mix']],
89
+ ['minMix', ['MinMix']],
90
+ ['maxMix', ['MaxMix']],
91
+ ['level', ['Level', 'OutputLevel']],
92
+ ['minOutputLevel', ['MinOutputLevel']],
93
+ ['maxOutputLevel', ['MaxOutputLevel']],
94
+ ['headroom', ['Headroom']],
95
+ ]);
96
+
97
+ const descriptor: MutableProperties = {};
98
+ addNumber(descriptor, 'preEmphasisGainDb', properties, ['DescriptorPreEmphasisGainDb', 'PreEmphasisGainDb']);
99
+ addString(descriptor, 'saturationMode', properties, ['SaturationMode']);
100
+ addNumber(descriptor, 'saturationPositiveScale', properties, ['SaturationPositiveScale']);
101
+ addNumber(descriptor, 'saturationNegativeScale', properties, ['SaturationNegativeScale']);
102
+ if (Object.keys(descriptor).length > 0) {
103
+ out.descriptor = descriptor;
104
+ }
105
+
106
+ const bands = parseActiveEqBands(properties);
107
+ if (bands.length > 0) {
108
+ out.bands = bands;
109
+ }
110
+ return out;
111
+ }
112
+
113
+ function delayChipDescriptor(properties: Readonly<Record<string, PropertyValue>>): MutableProperties {
114
+ const out = scalarDescriptor(properties, [
115
+ ['inputGain', ['InputGain']],
116
+ ['outputGain', ['OutputGain']],
117
+ ['minDelayMs', ['MinDelayMs']],
118
+ ['maxDelayMs', ['MaxDelayMs']],
119
+ ['defaultDelayMs', ['DefaultDelayMs', 'DelayMs']],
120
+ ['feedback', ['Feedback']],
121
+ ['minFeedback', ['MinFeedback']],
122
+ ['maxFeedback', ['MaxFeedback']],
123
+ ['mix', ['Mix']],
124
+ ['level', ['Level', 'OutputLevel']],
125
+ ['minOutputLevel', ['MinOutputLevel']],
126
+ ['maxOutputLevel', ['MaxOutputLevel']],
127
+ ['tone', ['Tone']],
128
+ ['modRateHz', ['ModRateHz']],
129
+ ['minModRateHz', ['MinModRateHz']],
130
+ ['maxModRateHz', ['MaxModRateHz']],
131
+ ['modDepthMs', ['ModDepthMs']],
132
+ ['minModDepthMs', ['MinModDepthMs']],
133
+ ['maxModDepthMs', ['MaxModDepthMs']],
134
+ ['inputDrive', ['InputDrive']],
135
+ ['headroom', ['Headroom']],
136
+ ['stereoOutputMode', ['StereoOutputMode']],
137
+ ]);
138
+ addBoolean(out, 'wetOnly', properties, ['WetOnly']);
139
+ addBoolean(out, 'dryUnity', properties, ['DryUnity']);
140
+ addBoolean(out, 'hold', properties, ['Hold']);
141
+ addBoolean(out, 'samplerRecordPlay', properties, ['SamplerRecordPlay']);
142
+
143
+ const mechanism: MutableProperties = {};
144
+ addString(mechanism, 'memoryType', properties, ['MemoryType']);
145
+ addNumber(mechanism, 'stageCount', properties, ['StageCount']);
146
+ addNumber(mechanism, 'artifactSeed', properties, ['ArtifactSeed']);
147
+ addNumber(mechanism, 'clockNoiseRms', properties, ['ClockNoiseRms']);
148
+ addNumber(mechanism, 'clockFeedthroughHz', properties, ['ClockFeedthroughHz']);
149
+ addNumber(mechanism, 'clockJitterDepthMs', properties, ['ClockJitterDepthMs']);
150
+ addNumber(mechanism, 'companderResidualAmount', properties, ['CompanderResidualAmount']);
151
+ addNumber(mechanism, 'baseBandwidthHz', properties, ['BaseBandwidthHz']);
152
+ addNumber(mechanism, 'darkBandwidthHz', properties, ['DarkBandwidthHz']);
153
+ addNumber(mechanism, 'feedbackLimit', properties, ['FeedbackLimit']);
154
+ addNumber(mechanism, 'quantizationBits', properties, ['QuantizationBits']);
155
+ addString(mechanism, 'memoryCompressionMode', properties, ['MemoryCompressionMode']);
156
+ addBoolean(mechanism, 'supplySensitive', properties, ['SupplySensitive']);
157
+ addNumber(mechanism, 'minDelayFloorMs', properties, ['MinDelayFloorMs']);
158
+ addString(mechanism, 'dryBlendPolicy', properties, ['DryBlendPolicy']);
159
+ if (Object.keys(mechanism).length > 0) {
160
+ out.mechanism = mechanism;
161
+ }
162
+ return out;
163
+ }
164
+
165
+ function reverbDescriptor(properties: Readonly<Record<string, PropertyValue>>): MutableProperties {
166
+ const out = scalarDescriptor(properties, [
167
+ ['inputGain', ['InputGain']],
168
+ ['outputGain', ['OutputGain']],
169
+ ['preDelayMs', ['PreDelayMs']],
170
+ ['decay', ['Decay']],
171
+ ['mix', ['Mix']],
172
+ ['level', ['Level', 'OutputLevel']],
173
+ ['minOutputLevel', ['MinOutputLevel']],
174
+ ['maxOutputLevel', ['MaxOutputLevel']],
175
+ ['tone', ['Tone']],
176
+ ['damping', ['Damping']],
177
+ ['size', ['Size']],
178
+ ['modRateHz', ['ModRateHz']],
179
+ ['modDepthMs', ['ModDepthMs']],
180
+ ['headroom', ['Headroom']],
181
+ ['stereoOutputMode', ['StereoOutputMode']],
182
+ ]);
183
+
184
+ const algorithm: MutableProperties = {};
185
+ addString(algorithm, 'profileAllPassMode', properties, ['ProfileAllPassMode']);
186
+ addNumberArray(algorithm, 'tankBaseMs', properties, ['TankBaseMs'], 4);
187
+ addNumberArray(algorithm, 'profileAllPassBaseMs', properties, ['ProfileAllPassBaseMs'], 2);
188
+ addNumberArray(algorithm, 'profileAllPassSizeMs', properties, ['ProfileAllPassSizeMs'], 2);
189
+ addNumber(algorithm, 'brightBandwidthHz', properties, ['BrightBandwidthHz']);
190
+ addNumber(algorithm, 'darkBandwidthHz', properties, ['DarkBandwidthHz']);
191
+ addNumber(algorithm, 'feedbackTrim', properties, ['FeedbackTrim']);
192
+ addNumber(algorithm, 'springFlutterBaseSamples', properties, ['SpringFlutterBaseSamples']);
193
+ addNumber(algorithm, 'springFlutterSizeSamples', properties, ['SpringFlutterSizeSamples']);
194
+ addNumber(algorithm, 'dampingMinimum', properties, ['DampingMinimum']);
195
+ addNumber(algorithm, 'dampingScale', properties, ['DampingScale']);
196
+ if (Object.keys(algorithm).length > 0) {
197
+ out.algorithm = algorithm;
198
+ }
199
+ return out;
200
+ }
201
+
202
+ function compressorDescriptor(properties: Readonly<Record<string, PropertyValue>>): MutableProperties {
203
+ const out = scalarDescriptor(properties, [
204
+ ['inputGain', ['InputGain']],
205
+ ['outputGain', ['OutputGain']],
206
+ ['detectorMode', ['DetectorMode']],
207
+ ['sensitivity', ['Sensitivity']],
208
+ ['minSensitivity', ['MinSensitivity']],
209
+ ['maxSensitivity', ['MaxSensitivity']],
210
+ ['level', ['Level', 'OutputLevel']],
211
+ ['minOutputLevel', ['MinOutputLevel']],
212
+ ['maxOutputLevel', ['MaxOutputLevel']],
213
+ ['attackMs', ['AttackMs']],
214
+ ['minAttackMs', ['MinAttackMs']],
215
+ ['maxAttackMs', ['MaxAttackMs']],
216
+ ['releaseMs', ['ReleaseMs']],
217
+ ['minReleaseMs', ['MinReleaseMs']],
218
+ ['maxReleaseMs', ['MaxReleaseMs']],
219
+ ['ratio', ['Ratio']],
220
+ ['thresholdDb', ['ThresholdDb']],
221
+ ['kneeDb', ['KneeDb']],
222
+ ['mix', ['Mix']],
223
+ ['tone', ['Tone']],
224
+ ['inputDrive', ['InputDrive']],
225
+ ['minInputDrive', ['MinInputDrive']],
226
+ ['maxInputDrive', ['MaxInputDrive']],
227
+ ['headroom', ['Headroom']],
228
+ ]);
229
+ const topology: MutableProperties = {};
230
+ addString(topology, 'topology', properties, ['Topology']);
231
+ addNumber(topology, 'otaProfileScale', properties, ['OtaProfileScale']);
232
+ addNumber(topology, 'makeupGainScale', properties, ['MakeupGainScale']);
233
+ if (Object.keys(topology).length > 0) {
234
+ out.topology = topology;
235
+ }
236
+ return out;
237
+ }
238
+
239
+ function octaveDescriptor(properties: Readonly<Record<string, PropertyValue>>): MutableProperties {
240
+ return scalarDescriptor(properties, [
241
+ ['inputGain', ['InputGain']],
242
+ ['outputGain', ['OutputGain']],
243
+ ['algorithm', ['Algorithm', 'Mechanism']],
244
+ ['dividerMode', ['DividerMode']],
245
+ ['dividerStages', ['DividerStages']],
246
+ ['trackerCutoffHz', ['TrackerCutoffHz']],
247
+ ['schmittHysteresis', ['SchmittHysteresis']],
248
+ ['gateThreshold', ['GateThreshold']],
249
+ ['gateRelease', ['GateRelease']],
250
+ ['square1CutoffHz', ['Square1CutoffHz']],
251
+ ['square2CutoffHz', ['Square2CutoffHz']],
252
+ ['chopperPreCutoffHz', ['ChopperPreCutoffHz']],
253
+ ['chopperPostCutoffHz', ['ChopperPostCutoffHz']],
254
+ ['chopperControlCutoffHz', ['ChopperControlCutoffHz']],
255
+ ['inputDrive', ['InputDrive']],
256
+ ['minInputDrive', ['MinInputDrive']],
257
+ ['maxInputDrive', ['MaxInputDrive']],
258
+ ['headroom', ['Headroom']],
259
+ ['mix', ['Mix']],
260
+ ['minMix', ['MinMix']],
261
+ ['maxMix', ['MaxMix']],
262
+ ['level', ['Level', 'OutputLevel']],
263
+ ['minOutputLevel', ['MinOutputLevel']],
264
+ ['maxOutputLevel', ['MaxOutputLevel']],
265
+ ['toneHz', ['ToneHz']],
266
+ ['minToneHz', ['MinToneHz']],
267
+ ['maxToneHz', ['MaxToneHz']],
268
+ ['directLevel', ['DirectLevel']],
269
+ ['oct1Level', ['Oct1Level']],
270
+ ['oct2Level', ['Oct2Level']],
271
+ ['carrierModRateHz', ['CarrierModRateHz']],
272
+ ['minCarrierModRateHz', ['MinCarrierModRateHz', 'MinLfoRateHz']],
273
+ ['maxCarrierModRateHz', ['MaxCarrierModRateHz', 'MaxLfoRateHz']],
274
+ ['carrierModAmount', ['CarrierModAmount', 'LfoAmount']],
275
+ ['carrierModShape', ['CarrierModShape', 'LfoShape']],
276
+ ]);
277
+ }
278
+
279
+ const COMMON_RUNTIME_FIELDS: readonly [string, readonly string[]][] = [
280
+ ['inputGain', ['InputGain']],
281
+ ['outputGain', ['OutputGain']],
282
+ ['sensitivity', ['Sensitivity']],
283
+ ['minSensitivity', ['MinSensitivity']],
284
+ ['maxSensitivity', ['MaxSensitivity']],
285
+ ['attackMs', ['AttackMs']],
286
+ ['minAttackMs', ['MinAttackMs']],
287
+ ['maxAttackMs', ['MaxAttackMs']],
288
+ ['releaseMs', ['ReleaseMs']],
289
+ ['minReleaseMs', ['MinReleaseMs']],
290
+ ['maxReleaseMs', ['MaxReleaseMs']],
291
+ ['triggerThreshold', ['TriggerThreshold']],
292
+ ['minGain', ['MinGain']],
293
+ ['level', ['Level', 'OutputLevel']],
294
+ ['minOutputLevel', ['MinOutputLevel']],
295
+ ['maxOutputLevel', ['MaxOutputLevel']],
296
+ ['headroom', ['Headroom']],
297
+ ['mix', ['Mix']],
298
+ ['minMix', ['MinMix']],
299
+ ['maxMix', ['MaxMix']],
300
+ ];
301
+
302
+ function scalarDescriptor(
303
+ properties: Readonly<Record<string, PropertyValue>>,
304
+ fields: readonly [string, readonly string[]][],
305
+ ): MutableProperties {
306
+ const out: MutableProperties = {};
307
+ for (const [target, sources] of fields) {
308
+ addScalar(out, target, properties, sources);
309
+ }
310
+ return out;
311
+ }
312
+
313
+ function addScalar(
314
+ out: MutableProperties,
315
+ target: string,
316
+ properties: Readonly<Record<string, PropertyValue>>,
317
+ sourceNames: readonly string[],
318
+ ): void {
319
+ const numberValue = firstNumber(properties, sourceNames);
320
+ if (numberValue !== undefined) {
321
+ out[target] = numberValue;
322
+ return;
323
+ }
324
+ addString(out, target, properties, sourceNames);
325
+ }
326
+
327
+ function addString(
328
+ out: MutableProperties,
329
+ target: string,
330
+ properties: Readonly<Record<string, PropertyValue>>,
331
+ sourceNames: readonly string[],
332
+ ): void {
333
+ const value = firstString(properties, sourceNames);
334
+ if (value !== undefined && value.length > 0) {
335
+ out[target] = value;
336
+ }
337
+ }
338
+
339
+ function addNumber(
340
+ out: MutableProperties,
341
+ target: string,
342
+ properties: Readonly<Record<string, PropertyValue>>,
343
+ sourceNames: readonly string[],
344
+ ): void {
345
+ const value = firstNumber(properties, sourceNames);
346
+ if (value !== undefined) {
347
+ out[target] = value;
348
+ }
349
+ }
350
+
351
+ function addBoolean(
352
+ out: MutableProperties,
353
+ target: string,
354
+ properties: Readonly<Record<string, PropertyValue>>,
355
+ sourceNames: readonly string[],
356
+ ): void {
357
+ const value = firstBoolean(properties, sourceNames);
358
+ if (value !== undefined) {
359
+ out[target] = value;
360
+ }
361
+ }
362
+
363
+ function addNumberArray(
364
+ out: MutableProperties,
365
+ target: string,
366
+ properties: Readonly<Record<string, PropertyValue>>,
367
+ sourceNames: readonly string[],
368
+ expectedLength: number,
369
+ ): void {
370
+ const values = firstNumberList(properties, sourceNames);
371
+ if (values.length === expectedLength) {
372
+ out[target] = values;
373
+ }
374
+ }
375
+
376
+ function firstString(
377
+ properties: Readonly<Record<string, PropertyValue>>,
378
+ sourceNames: readonly string[],
379
+ ): string | undefined {
380
+ for (const sourceName of sourceNames) {
381
+ const value = propertyStringValue(properties[sourceName]);
382
+ if (value !== null) {
383
+ return value;
384
+ }
385
+ }
386
+ return undefined;
387
+ }
388
+
389
+ function firstNumber(
390
+ properties: Readonly<Record<string, PropertyValue>>,
391
+ sourceNames: readonly string[],
392
+ ): number | undefined {
393
+ for (const sourceName of sourceNames) {
394
+ const value = propertyNumericValue(properties[sourceName]);
395
+ if (value !== undefined) {
396
+ return value;
397
+ }
398
+ }
399
+ return undefined;
400
+ }
401
+
402
+ function firstBoolean(
403
+ properties: Readonly<Record<string, PropertyValue>>,
404
+ sourceNames: readonly string[],
405
+ ): boolean | undefined {
406
+ for (const sourceName of sourceNames) {
407
+ const value = propertyBooleanValue(properties[sourceName]);
408
+ if (value !== undefined) {
409
+ return value;
410
+ }
411
+ }
412
+ return undefined;
413
+ }
414
+
415
+ function firstStringList(
416
+ properties: Readonly<Record<string, PropertyValue>>,
417
+ sourceNames: readonly string[],
418
+ ): readonly string[] {
419
+ const value = firstString(properties, sourceNames);
420
+ if (value === undefined) {
421
+ return [];
422
+ }
423
+ return value
424
+ .split(',')
425
+ .map((item) => item.trim())
426
+ .filter((item) => item.length > 0);
427
+ }
428
+
429
+ function firstNumberList(
430
+ properties: Readonly<Record<string, PropertyValue>>,
431
+ sourceNames: readonly string[],
432
+ ): readonly number[] {
433
+ const value = firstString(properties, sourceNames);
434
+ if (value === undefined) {
435
+ return [];
436
+ }
437
+ return parseNumberList(value);
438
+ }
439
+
440
+ function parseNumberList(value: string): readonly number[] {
441
+ return value
442
+ .split(',')
443
+ .map((item) => Number.parseFloat(item.trim()))
444
+ .filter((item) => Number.isFinite(item));
445
+ }
446
+
447
+ function parseToneStackSections(properties: Readonly<Record<string, PropertyValue>>): readonly MutableProperties[] {
448
+ const value = firstString(properties, ['ToneStackSections', 'Sections']);
449
+ if (value === undefined) {
450
+ return [];
451
+ }
452
+ return value
453
+ .split(';')
454
+ .map((section) => parseToneStackSection(section))
455
+ .filter((section) => section !== null);
456
+ }
457
+
458
+ function parseToneStackSection(value: string): MutableProperties | null {
459
+ const [gain, zeroHz, poleHz] = parseNumberList(value);
460
+ if (gain === undefined || zeroHz === undefined || poleHz === undefined) {
461
+ return null;
462
+ }
463
+ return { gain, zeroHz, poleHz };
464
+ }
465
+
466
+ function parseActiveEqBands(properties: Readonly<Record<string, PropertyValue>>): readonly MutableProperties[] {
467
+ const frequencies = firstNumberList(properties, ['BandFrequenciesHz']);
468
+ if (frequencies.length === 0) {
469
+ return [];
470
+ }
471
+
472
+ const types = firstStringList(properties, ['BandFilterTypes']);
473
+ const minFrequencies = firstNumberList(properties, ['MinBandFrequenciesHz']);
474
+ const maxFrequencies = firstNumberList(properties, ['MaxBandFrequenciesHz']);
475
+ const gains = firstNumberList(properties, ['BandGainsDb']);
476
+ const minGains = firstNumberList(properties, ['MinBandGainsDb', 'MinGainDb']);
477
+ const maxGains = firstNumberList(properties, ['MaxBandGainsDb', 'MaxGainDb']);
478
+ const qValues = firstNumberList(properties, ['BandQ', 'Q']);
479
+ const labels = firstStringList(properties, ['BandLabels']);
480
+
481
+ return frequencies.map((frequencyHz, index) => {
482
+ const band: MutableProperties = {
483
+ type: types[index] ?? 'peaking',
484
+ frequencyHz,
485
+ minFrequencyHz: numberAt(minFrequencies, index, frequencyHz),
486
+ maxFrequencyHz: numberAt(maxFrequencies, index, frequencyHz),
487
+ gainDb: numberAt(gains, index, 0),
488
+ minGainDb: numberAt(minGains, index, -15),
489
+ maxGainDb: numberAt(maxGains, index, 15),
490
+ q: numberAt(qValues, index, 1),
491
+ };
492
+ const label = labels[index];
493
+ if (label !== undefined) {
494
+ band.label = label;
495
+ }
496
+ return band;
497
+ });
498
+ }
499
+
500
+ function numberAt(values: readonly number[], index: number, fallback: number): number {
501
+ return values[index] ?? values[0] ?? fallback;
502
+ }