@effetune/dsp 0.0.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/dist/preset.js ADDED
@@ -0,0 +1,518 @@
1
+ import { getEffectCatalog, getEffectImplementation } from './catalog.js';
2
+ import { normalizeChainDocument } from './semantics.js';
3
+ import { EffectError, ValidationError } from './errors.js';
4
+
5
+ function isRecord(value) {
6
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
7
+ }
8
+
9
+ function parseJson(value, label) {
10
+ if (typeof value !== 'string') return value;
11
+ try {
12
+ return JSON.parse(value);
13
+ } catch (error) {
14
+ throw new ValidationError(`${label} is not valid JSON.`, { cause: error });
15
+ }
16
+ }
17
+
18
+ // App display names whose alphanumeric normalization cannot reach the semantic type
19
+ // or the internal type (leading digits instead of spelled-out numbers). Keys are the
20
+ // exact display names produced by the app's plugin constructors; the Python binding
21
+ // keeps the same table in dsp/bindings/python/src/effetune/presets.py and both are
22
+ // pinned to dsp/bindings/common/legacy-app-export-v1.fixture.json.
23
+ export const LEGACY_EFFECT_ALIASES_V1 = Object.freeze({
24
+ '5Band PEQ': 'FiveBandPEQ',
25
+ '15Band GEQ': 'FifteenBandGEQ',
26
+ '15Band PEQ': 'FifteenBandPEQ',
27
+ '5Band Dynamic EQ': 'FiveBandDynamicEQ',
28
+ '5Band FIR PEQ': 'FiveBandFIRPEQ'
29
+ });
30
+
31
+ const SHORT_FORMAT_GUIDANCE_V1 =
32
+ "short-format presets shared through the EffeTune app's URL or clipboard are not " +
33
+ 'supported. Export a .effetune_preset file (long format) from the app and import ' +
34
+ 'that instead.';
35
+
36
+ function rejectShortFormat(label) {
37
+ throw new ValidationError(`${label} uses short-format keys (nm/en); ${SHORT_FORMAT_GUIDANCE_V1}`);
38
+ }
39
+
40
+ export function parsePreset(value) {
41
+ // App preset envelopes are reported by normalizeChainDocument so that createChain
42
+ // and the AudioWorklet loader give the same guidance from a single implementation.
43
+ return normalizeChainDocument(parseJson(value, 'Preset'));
44
+ }
45
+
46
+ function legacyChannel(value) {
47
+ if (value === undefined || value === null || value === '') return 'stereo';
48
+ const mapping = {
49
+ A: 'all',
50
+ All: 'all',
51
+ all: 'all',
52
+ L: 'left',
53
+ Left: 'left',
54
+ left: 'left',
55
+ R: 'right',
56
+ Right: 'right',
57
+ right: 'right',
58
+ stereo: 'stereo'
59
+ };
60
+ const normalized = mapping[value] ?? value;
61
+ if (['1', '2', '3', '4', '5', '6', '7', '8', '34', '56', '78'].includes(normalized)) {
62
+ return normalized;
63
+ }
64
+ if (['all', 'stereo', 'left', 'right'].includes(normalized)) return normalized;
65
+ throw new ValidationError(`Unsupported legacy channel: ${String(value)}`);
66
+ }
67
+
68
+ function reverseTransform(transform, value) {
69
+ const kind = transform?.kind ?? 'identity';
70
+ if (kind === 'naturalLog' || kind === 'log10' || kind === 'decibelsFromReference') {
71
+ // These inverse transforms are arithmetic, and JavaScript coerces its way into a
72
+ // number for values the app never wrote: null and [] become 0, true becomes 1. Some
73
+ // of those land inside the parameter's accepted range, so without this guard the
74
+ // import would silently invent a setting. The Python binding rejects the same values
75
+ // in effetune.presets._legacy_parameters.
76
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
77
+ throw new ValidationError('A legacy parameter value is not a finite number.');
78
+ }
79
+ }
80
+ switch (kind) {
81
+ case 'identity':
82
+ return value;
83
+ case 'naturalLog':
84
+ return Math.exp(value);
85
+ case 'log10':
86
+ return 10 ** value;
87
+ case 'decibelsFromReference':
88
+ return transform.reference * (10 ** (value / 20));
89
+ case 'map': {
90
+ const mapped = transform.values.find(entry => Object.is(entry.internal, value));
91
+ if (!mapped) throw new ValidationError('A legacy parameter value cannot be converted.');
92
+ return mapped.public;
93
+ }
94
+ default:
95
+ throw new ValidationError(`Unsupported legacy transform: ${String(transform?.kind)}`);
96
+ }
97
+ }
98
+
99
+ function buildTypeLookup() {
100
+ const lookup = new Map();
101
+ for (const effect of getEffectCatalog().effects) {
102
+ const normalized = effect.type.replace(/[^a-z0-9]/gi, '').toLowerCase();
103
+ const implementation = getEffectImplementation(effect.type);
104
+ lookup.set(effect.type, effect);
105
+ lookup.set(implementation.internalType, effect);
106
+ lookup.set(normalized, effect);
107
+ lookup.set(implementation.internalType.replace(/[^a-z0-9]/gi, '').toLowerCase(), effect);
108
+ }
109
+ for (const [displayName, type] of Object.entries(LEGACY_EFFECT_ALIASES_V1)) {
110
+ const effect = lookup.get(type);
111
+ if (!effect) throw new EffectError(`Unsupported legacy effect alias target: ${type}`);
112
+ lookup.set(displayName, effect);
113
+ }
114
+ return lookup;
115
+ }
116
+
117
+ function resolveLegacyType(value, lookup) {
118
+ if (typeof value !== 'string' || value.length === 0) {
119
+ throw new ValidationError('Legacy effect entry is missing its name.');
120
+ }
121
+ const normalized = value.replace(/[^a-z0-9]/gi, '').toLowerCase();
122
+ const effect = lookup.get(value) ?? lookup.get(normalized);
123
+ if (!effect) throw new EffectError(`Unsupported legacy effect: ${value}`);
124
+ return effect;
125
+ }
126
+
127
+ // Reject app presets for effects the library drives from a precomputed asset. These
128
+ // effects replace the app's filter-design parameters with an impulse response supplied
129
+ // by the caller, so an app export carries nothing convertible: reporting the app's
130
+ // parameters as unsupported fields would read like a typo report instead of the
131
+ // structural limit it is. The set is derived from the generated catalog's required
132
+ // assets rather than a second hardcoded table. The Python binding keeps the same rule
133
+ // in effetune.presets._reject_asset_backed_legacy_effect_v1 -- same error type, same
134
+ // message -- and both are pinned to
135
+ // dsp/bindings/common/legacy-app-export-v1.fixture.json.
136
+ function rejectAssetBackedLegacyEffectV1(definition) {
137
+ if (!(definition.assets ?? []).some(asset => asset.required)) return;
138
+ throw new EffectError(
139
+ `${definition.type} cannot be imported from an EffeTune app preset: in the DSP ` +
140
+ 'library this effect is driven by a precomputed impulse-response asset, and ' +
141
+ "the app's filter-design parameters cannot be converted. Construct the effect " +
142
+ 'directly and supply its impulse-response asset instead.'
143
+ );
144
+ }
145
+
146
+ // The app stores a few effects' per-band settings as an array of objects instead of
147
+ // the flat numbered keys every other effect uses. Expanding them here keeps the rest
148
+ // of the importer working on a single shape. The Python binding keeps the same table
149
+ // in effetune.presets._prepare_legacy_parameters_v1 and both are pinned to
150
+ // dsp/bindings/common/legacy-app-export-v1.fixture.json.
151
+ const LEGACY_OBJECT_ARRAYS_V1 = Object.freeze({
152
+ MultibandCompressor: Object.freeze({
153
+ effectLabel: 'Multiband Compressor',
154
+ arrayKey: 'bands',
155
+ count: 5,
156
+ members: Object.freeze({
157
+ t: 'threshold',
158
+ r: 'ratio',
159
+ a: 'attack',
160
+ rl: 'release',
161
+ k: 'knee',
162
+ g: 'gain'
163
+ }),
164
+ // The app also persists the band meter readout; it is display state, not a
165
+ // processing parameter, so it is accepted and discarded.
166
+ allowMeterState: true
167
+ }),
168
+ MultibandExpander: Object.freeze({
169
+ effectLabel: 'Multiband Expander',
170
+ arrayKey: 'bands',
171
+ count: 5,
172
+ members: Object.freeze({
173
+ t: 'threshold',
174
+ r: 'ratio',
175
+ a: 'attack',
176
+ rl: 'release',
177
+ k: 'knee',
178
+ g: 'gain'
179
+ })
180
+ }),
181
+ MultibandTransient: Object.freeze({
182
+ effectLabel: 'Multiband Transient',
183
+ arrayKey: 'bands',
184
+ count: 3,
185
+ members: Object.freeze({
186
+ fa: 'fastAttack',
187
+ fr: 'fastRelease',
188
+ sa: 'slowAttack',
189
+ sr: 'slowRelease',
190
+ gt: 'transientGain',
191
+ gs: 'sustainGain',
192
+ sm: 'gainSmoothing'
193
+ })
194
+ }),
195
+ MultibandBalance: Object.freeze({
196
+ effectLabel: 'Multiband Balance',
197
+ arrayKey: 'bands',
198
+ count: 5,
199
+ members: Object.freeze({ balance: 'balance' })
200
+ }),
201
+ MultibandSaturation: Object.freeze({
202
+ effectLabel: 'Multiband Saturation',
203
+ arrayKey: 'bands',
204
+ count: 3,
205
+ members: Object.freeze({ dr: 'drive', bs: 'bias', mx: 'mix', gn: 'gain' })
206
+ }),
207
+ FiveBandDynamicEQ: Object.freeze({
208
+ effectLabel: '5Band Dynamic EQ',
209
+ arrayKey: 'bs',
210
+ count: 5,
211
+ members: Object.freeze({
212
+ en: 'enabledBands',
213
+ ft: 'filterType',
214
+ f: 'frequency',
215
+ q: 'q',
216
+ mg: 'maxGain',
217
+ th: 'threshold',
218
+ r: 'ratio',
219
+ kn: 'knee',
220
+ a: 'attack',
221
+ rl: 'release',
222
+ scf: 'sidechainFrequency',
223
+ scq: 'sidechainQ'
224
+ })
225
+ }),
226
+ ModalResonator: Object.freeze({
227
+ effectLabel: 'Modal Resonator',
228
+ arrayKey: 'rs',
229
+ count: 5,
230
+ members: Object.freeze({
231
+ en: 'resonatorEnabled',
232
+ fr: 'frequencyLog',
233
+ dc: 'decay',
234
+ lp: 'lowPassLog',
235
+ hp: 'highPassLog',
236
+ gn: 'gain'
237
+ }),
238
+ itemLabel: 'resonator'
239
+ })
240
+ });
241
+
242
+ // The app's serializer copies structural values into the parameters object it
243
+ // exports: plugin-base.js getSerializableParameters() re-emits the assigned channel
244
+ // and bus indexes under their short names, and a few plugins add their class name
245
+ // under `pluginType`. Every one of them duplicates a value the surrounding node
246
+ // already carries, which stays the source of truth, so they are dropped instead of
247
+ // being reported as unknown parameters. The Python binding keeps the same table in
248
+ // effetune.presets._LEGACY_ECHOED_STRUCTURAL_KEYS_V1.
249
+ const LEGACY_ECHOED_STRUCTURAL_KEYS_V1 = Object.freeze(['pluginType', 'ch', 'ib', 'ob']);
250
+
251
+ // Horn Resonator and Horn Resonator Plus additionally echo the node's enabled switch
252
+ // into their parameters. Unlike Modal Resonator's `en` -- an independent processing
253
+ // switch the importer folds into `enabled` -- it is a plain duplicate, so it is
254
+ // dropped for these two effects only.
255
+ const LEGACY_ECHOED_ENABLED_EFFECTS_V1 = Object.freeze(['HornResonator', 'HornResonatorPlus']);
256
+
257
+ function dropEchoedStructuralKeysV1(parameters, effectType) {
258
+ for (const key of LEGACY_ECHOED_STRUCTURAL_KEYS_V1) delete parameters[key];
259
+ if (LEGACY_ECHOED_ENABLED_EFFECTS_V1.includes(effectType)) delete parameters.en;
260
+ return parameters;
261
+ }
262
+
263
+ // A few effects store a fixed-length per-channel setting as one short key holding the
264
+ // whole array instead of the numbered keys every other effect uses. The array length
265
+ // itself is checked by the shared chain-document validation. The Python binding keeps
266
+ // the same table in effetune.presets._prepare_legacy_parameters_v1.
267
+ const LEGACY_SHORT_KEY_ARRAYS_V1 = Object.freeze({
268
+ MultiChannelPanel: Object.freeze({
269
+ effectLabel: 'MultiChannel Panel',
270
+ itemLabel: 'channel',
271
+ members: Object.freeze({ m: 'mute', s: 'solo', v: 'volume', d: 'delay', l: 'link' })
272
+ })
273
+ });
274
+
275
+ function expandLegacyShortKeyArraysV1(parameters, { effectLabel, itemLabel, members }) {
276
+ for (const [legacyName, publicName] of Object.entries(members)) {
277
+ if (!Object.hasOwn(parameters, legacyName)) continue;
278
+ if (Object.hasOwn(parameters, publicName)) {
279
+ throw new ValidationError(
280
+ `Legacy ${effectLabel} supplies the same ${itemLabel} settings more than once.`
281
+ );
282
+ }
283
+ const values = parameters[legacyName];
284
+ delete parameters[legacyName];
285
+ if (!Array.isArray(values)) {
286
+ throw new ValidationError(
287
+ `Legacy ${effectLabel} contains unsupported or incomplete ${itemLabel} settings.`
288
+ );
289
+ }
290
+ parameters[publicName] = values;
291
+ }
292
+ }
293
+
294
+ function expandLegacyObjectArrayV1(parameters, {
295
+ effectLabel,
296
+ arrayKey,
297
+ count,
298
+ members,
299
+ itemLabel = 'band',
300
+ allowMeterState = false
301
+ }) {
302
+ if (!Object.hasOwn(parameters, arrayKey)) return;
303
+ const values = parameters[arrayKey];
304
+ delete parameters[arrayKey];
305
+ if (!Array.isArray(values) || values.length !== count) {
306
+ throw new ValidationError(
307
+ `Legacy ${effectLabel} must contain exactly ${count} ${itemLabel} settings.`
308
+ );
309
+ }
310
+ const publicNames = Object.values(members);
311
+ if (publicNames.some(name => Object.hasOwn(parameters, name))) {
312
+ throw new ValidationError(
313
+ `Legacy ${effectLabel} supplies the same ${itemLabel} settings more than once.`
314
+ );
315
+ }
316
+ const expanded = new Map(publicNames.map(name => [name, []]));
317
+ const required = Object.keys(members);
318
+ const allowed = new Set(allowMeterState ? [...required, 'gr'] : required);
319
+ for (const value of values) {
320
+ if (!isRecord(value) ||
321
+ Object.keys(value).some(key => !allowed.has(key)) ||
322
+ required.some(key => !Object.hasOwn(value, key))) {
323
+ throw new ValidationError(
324
+ `Legacy ${effectLabel} contains unsupported or incomplete ${itemLabel} settings.`
325
+ );
326
+ }
327
+ for (const [legacyName, publicName] of Object.entries(members)) {
328
+ expanded.get(publicName).push(value[legacyName]);
329
+ }
330
+ if (Object.hasOwn(value, 'gr') &&
331
+ (typeof value.gr !== 'number' || !Number.isFinite(value.gr))) {
332
+ throw new ValidationError(
333
+ `Legacy ${effectLabel} contains invalid meter display state.`
334
+ );
335
+ }
336
+ }
337
+ for (const [name, collected] of expanded) parameters[name] = collected;
338
+ }
339
+
340
+ function prepareLegacyParametersV1(effectType, source) {
341
+ const parameters = dropEchoedStructuralKeysV1({ ...source }, effectType);
342
+ let processingEnabled = true;
343
+ if (effectType === 'Matrix' && Object.hasOwn(parameters, 'mx')) {
344
+ if (Object.hasOwn(parameters, 'matrixRoutes')) {
345
+ throw new ValidationError('Legacy Matrix supplies routing settings more than once.');
346
+ }
347
+ parameters.matrixRoutes = parameters.mx;
348
+ delete parameters.mx;
349
+ return { parameters, processingEnabled };
350
+ }
351
+ if (Object.hasOwn(LEGACY_SHORT_KEY_ARRAYS_V1, effectType)) {
352
+ expandLegacyShortKeyArraysV1(parameters, LEGACY_SHORT_KEY_ARRAYS_V1[effectType]);
353
+ return { parameters, processingEnabled };
354
+ }
355
+ const objectArray = Object.hasOwn(LEGACY_OBJECT_ARRAYS_V1, effectType)
356
+ ? LEGACY_OBJECT_ARRAYS_V1[effectType]
357
+ : null;
358
+ if (!objectArray) return { parameters, processingEnabled };
359
+ expandLegacyObjectArrayV1(parameters, objectArray);
360
+ if (effectType === 'ModalResonator') {
361
+ // The app persists the whole effect's bypass switch and the editor's selected
362
+ // resonator alongside the processing parameters.
363
+ if (Object.hasOwn(parameters, 'en')) {
364
+ processingEnabled = parameters.en;
365
+ delete parameters.en;
366
+ if (typeof processingEnabled !== 'boolean') {
367
+ throw new ValidationError('Legacy Modal Resonator processing state must be a boolean.');
368
+ }
369
+ }
370
+ if (Object.hasOwn(parameters, 'sr')) {
371
+ const selection = parameters.sr;
372
+ delete parameters.sr;
373
+ if (!Number.isInteger(selection) || selection < 0 || selection >= 5) {
374
+ throw new ValidationError(
375
+ 'Legacy Modal Resonator editor selection must be an integer from 0 to 4.'
376
+ );
377
+ }
378
+ }
379
+ }
380
+ return { parameters, processingEnabled };
381
+ }
382
+
383
+ function legacyParameters(definition, implementation, source) {
384
+ if (!isRecord(source)) throw new ValidationError(`${definition.type} legacy parameters must be an object.`);
385
+ const consumed = new Set();
386
+ const output = {};
387
+ for (const parameter of definition.parameters) {
388
+ if (Object.hasOwn(source, parameter.name)) {
389
+ output[parameter.name] = source[parameter.name];
390
+ consumed.add(parameter.name);
391
+ continue;
392
+ }
393
+ const mapping = implementation.packedParameters.find(entry => entry.publicName === parameter.name);
394
+ if (!mapping) continue;
395
+ if (mapping.count === 1) {
396
+ const key = [mapping.field, ...mapping.keys].find(candidate => Object.hasOwn(source, candidate));
397
+ if (key !== undefined) {
398
+ output[parameter.name] = reverseTransform(mapping.transform, source[key]);
399
+ consumed.add(key);
400
+ }
401
+ } else {
402
+ const field = source[mapping.field];
403
+ if (Array.isArray(field)) {
404
+ output[parameter.name] = field.map(value => reverseTransform(mapping.transform, value));
405
+ consumed.add(mapping.field);
406
+ } else if (mapping.keys.every(key => Object.hasOwn(source, key))) {
407
+ output[parameter.name] = mapping.keys.map(key => {
408
+ consumed.add(key);
409
+ return reverseTransform(mapping.transform, source[key]);
410
+ });
411
+ }
412
+ }
413
+ }
414
+ const ignored = new Set(['type', 'id', 'enabled', 'inputBus', 'outputBus', 'channel']);
415
+ for (const key of Object.keys(source)) {
416
+ if (!consumed.has(key) && !ignored.has(key)) {
417
+ throw new ValidationError(`Unsupported legacy parameter ${definition.type}.${key}.`);
418
+ }
419
+ }
420
+ return output;
421
+ }
422
+
423
+ export function importLegacyPreset(value) {
424
+ let preset = parseJson(value, 'Legacy preset');
425
+ if (Array.isArray(preset)) {
426
+ // The app still loads a preset file whose top level is a bare array of
427
+ // long-format entries as the pipeline (js/electron/presetIntegration.js and
428
+ // js/app.js), so files in that shape exist in the wild. A bare array of
429
+ // short-format entries keeps the existing guidance instead. The first entry that
430
+ // is an object decides, exactly like the Python binding.
431
+ const first = preset.find(entry => isRecord(entry));
432
+ if (first && Object.hasOwn(first, 'nm') && !Object.hasOwn(first, 'name')) {
433
+ rejectShortFormat('Legacy preset');
434
+ }
435
+ preset = { pipeline: preset };
436
+ }
437
+ if (!isRecord(preset)) throw new ValidationError('Legacy preset must be an object.');
438
+ const hasLong = Array.isArray(preset.pipeline);
439
+ const hasShort = Array.isArray(preset.plugins);
440
+ if (hasLong === hasShort) {
441
+ throw new ValidationError('Legacy preset must contain exactly one pipeline or plugins array.');
442
+ }
443
+ const allowedTop = new Set(['name', 'timestamp', hasLong ? 'pipeline' : 'plugins']);
444
+ for (const key of Object.keys(preset)) {
445
+ if (!allowedTop.has(key)) throw new ValidationError(`Unsupported legacy preset field: ${key}`);
446
+ }
447
+
448
+ const lookup = buildTypeLookup();
449
+ const entries = [];
450
+ let sectionEnabled = true;
451
+ for (const [index, entry] of (hasLong ? preset.pipeline : preset.plugins).entries()) {
452
+ if (!isRecord(entry)) throw new ValidationError(`Legacy entry ${index} must be an object.`);
453
+ const allowedEntry = new Set(hasLong
454
+ ? ['id', 'name', 'enabled', 'parameters', 'inputBus', 'outputBus', 'channel']
455
+ : ['nm', 'en', 'ib', 'ob', 'ch']);
456
+ if (hasLong) {
457
+ if (Object.hasOwn(entry, 'nm') && !Object.hasOwn(entry, 'name')) {
458
+ rejectShortFormat(`Legacy pipeline[${index}]`);
459
+ }
460
+ for (const key of Object.keys(entry)) {
461
+ if (!allowedEntry.has(key)) {
462
+ throw new ValidationError(`Unsupported legacy effect field: ${key}`);
463
+ }
464
+ }
465
+ }
466
+ const name = hasLong ? entry.name : entry.nm;
467
+ const inputBus = hasLong ? entry.inputBus : entry.ib;
468
+ const outputBus = hasLong ? entry.outputBus : entry.ob;
469
+ if ((inputBus !== undefined && inputBus !== 0) ||
470
+ (outputBus !== undefined && outputBus !== 0)) {
471
+ throw new ValidationError(`Legacy entry ${index} uses bus routing that a serial chain cannot represent.`);
472
+ }
473
+ // A non-string name is not a Section: leave it to resolveLegacyType so it is
474
+ // reported as a missing name instead of escaping as a raw TypeError.
475
+ if (typeof name === 'string' && name.replace(/[^a-z0-9]/gi, '').toLowerCase() === 'section') {
476
+ const sectionParameters = hasLong ? entry.parameters ?? {} : entry;
477
+ if (!isRecord(sectionParameters)) {
478
+ throw new ValidationError('Legacy Section parameters must be an object.');
479
+ }
480
+ const structural = hasLong
481
+ ? new Set()
482
+ : new Set(['nm', 'en', 'ib', 'ob', 'ch']);
483
+ for (const key of Object.keys(sectionParameters)) {
484
+ if (!structural.has(key) &&
485
+ !['cm', 'comment'].includes(key) &&
486
+ !LEGACY_ECHOED_STRUCTURAL_KEYS_V1.includes(key)) {
487
+ throw new ValidationError(`Unsupported legacy Section parameter: ${key}`);
488
+ }
489
+ }
490
+ const enabled = (hasLong ? entry.enabled : entry.en) ?? true;
491
+ if (typeof enabled !== 'boolean') {
492
+ throw new ValidationError('Legacy Section enabled must be boolean.');
493
+ }
494
+ sectionEnabled = enabled;
495
+ continue;
496
+ }
497
+ const definition = resolveLegacyType(name, lookup);
498
+ rejectAssetBackedLegacyEffectV1(definition);
499
+ const implementation = getEffectImplementation(definition.type);
500
+ const source = hasLong
501
+ ? entry.parameters ?? {}
502
+ : Object.fromEntries(Object.entries(entry).filter(([key]) =>
503
+ !['nm', 'en', 'ib', 'ob', 'ch'].includes(key)
504
+ ));
505
+ if (!isRecord(source)) {
506
+ throw new ValidationError(`Legacy ${definition.type} parameters must be an object.`);
507
+ }
508
+ const { parameters, processingEnabled } = prepareLegacyParametersV1(definition.type, source);
509
+ entries.push({
510
+ ...(hasLong && entry.id !== undefined ? { id: entry.id } : {}),
511
+ type: definition.type,
512
+ enabled: ((hasLong ? entry.enabled : entry.en) ?? true) && sectionEnabled && processingEnabled,
513
+ channel: legacyChannel(hasLong ? entry.channel : entry.ch),
514
+ parameters: legacyParameters(definition, implementation, parameters)
515
+ });
516
+ }
517
+ return normalizeChainDocument({ version: 1, chain: entries });
518
+ }