@effetune/dsp 0.1.0 → 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.
@@ -174,6 +174,7 @@ export function createDspParamPackers(generatedModule, kernels = null, warning =
174
174
  const byteCapacity = Number.isInteger(value?.byteCapacity) ? value.byteCapacity : 0;
175
175
  const hash = Number.isInteger(value?.hash) ? value.hash >>> 0 : findHash(generatedModule, typeName);
176
176
  if (typeof pack !== 'function' || hash === null) continue;
177
+ if (kernelHashes && !kernelHashes.has(typeName)) continue;
177
178
  if (kernelHashes && (kernelHashes.get(typeName) !== hash ||
178
179
  kernelByteCapacities.get(typeName) !== byteCapacity)) {
179
180
  warn(warning, `${typeName} generated parameter layout does not match the loaded kernel`);
@@ -194,6 +195,7 @@ export function createDspParamPackers(generatedModule, kernels = null, warning =
194
195
  const typeName = match[1];
195
196
  const hash = findHash(generatedModule, typeName);
196
197
  if (hash === null) continue;
198
+ if (kernelHashes && !kernelHashes.has(typeName)) continue;
197
199
  if (kernelHashes && kernelHashes.get(typeName) !== hash) {
198
200
  warn(warning, `${typeName} generated parameter layout does not match the loaded kernel`);
199
201
  continue;
package/dist/preset.js CHANGED
@@ -198,6 +198,24 @@ const LEGACY_OBJECT_ARRAYS_V1 = Object.freeze({
198
198
  count: 5,
199
199
  members: Object.freeze({ balance: 'balance' })
200
200
  }),
201
+ PhaseSelectEQ: Object.freeze({
202
+ effectLabel: 'Phase Select EQ',
203
+ arrayKey: 'regions',
204
+ count: 5,
205
+ members: Object.freeze({
206
+ en: 'regionEnabled',
207
+ ofl: 'outerFrequencyLow',
208
+ fl: 'coreFrequencyLow',
209
+ fh: 'coreFrequencyHigh',
210
+ ofh: 'outerFrequencyHigh',
211
+ opl: 'outerPhaseLow',
212
+ pl: 'corePhaseLow',
213
+ ph: 'corePhaseHigh',
214
+ oph: 'outerPhaseHigh',
215
+ gn: 'gain'
216
+ }),
217
+ itemLabel: 'region'
218
+ }),
201
219
  MultibandSaturation: Object.freeze({
202
220
  effectLabel: 'Multiband Saturation',
203
221
  arrayKey: 'bands',
@@ -254,6 +272,10 @@ const LEGACY_ECHOED_STRUCTURAL_KEYS_V1 = Object.freeze(['pluginType', 'ch', 'ib'
254
272
  // dropped for these two effects only.
255
273
  const LEGACY_ECHOED_ENABLED_EFFECTS_V1 = Object.freeze(['HornResonator', 'HornResonatorPlus']);
256
274
 
275
+ // These analyzers persist their frequency-axis choice for display only. Validate
276
+ // the known values before discarding it so other unknown parameters remain strict.
277
+ const LEGACY_FREQUENCY_SCALE_EFFECTS_V1 = Object.freeze(['SpectrumAnalyzer', 'Spectrogram']);
278
+
257
279
  function dropEchoedStructuralKeysV1(parameters, effectType) {
258
280
  for (const key of LEGACY_ECHOED_STRUCTURAL_KEYS_V1) delete parameters[key];
259
281
  if (LEGACY_ECHOED_ENABLED_EFFECTS_V1.includes(effectType)) delete parameters.en;
@@ -339,6 +361,13 @@ function expandLegacyObjectArrayV1(parameters, {
339
361
 
340
362
  function prepareLegacyParametersV1(effectType, source) {
341
363
  const parameters = dropEchoedStructuralKeysV1({ ...source }, effectType);
364
+ if (LEGACY_FREQUENCY_SCALE_EFFECTS_V1.includes(effectType) &&
365
+ Object.hasOwn(parameters, 'sc')) {
366
+ if (parameters.sc !== 'log' && parameters.sc !== 'linear') {
367
+ throw new ValidationError(`Legacy ${effectType} contains invalid frequency scale display state.`);
368
+ }
369
+ delete parameters.sc;
370
+ }
342
371
  let processingEnabled = true;
343
372
  if (effectType === 'Matrix' && Object.hasOwn(parameters, 'mx')) {
344
373
  if (Object.hasOwn(parameters, 'matrixRoutes')) {
package/dist/runtime.js CHANGED
@@ -4,6 +4,7 @@ import { createEngineSession } from './engine.js';
4
4
  import { StateError, ValidationError } from './errors.js';
5
5
  import {
6
6
  normalizeChainDocument,
7
+ canonicalizeEffectForProcessing,
7
8
  channelRange,
8
9
  packEffect,
9
10
  setEffectParameter,
@@ -90,18 +91,31 @@ function updateEffectParameters(effect, parameters) {
90
91
  return updated;
91
92
  }
92
93
 
94
+ function updateProcessingEffectParameters(effect, parameters) {
95
+ return canonicalizeEffectForProcessing(updateEffectParameters(effect, parameters));
96
+ }
97
+
98
+ function canonicalizeDocumentForProcessing(document) {
99
+ return {
100
+ version: 1,
101
+ chain: document.chain.map(canonicalizeEffectForProcessing)
102
+ };
103
+ }
104
+
93
105
  function replaceEffect(document, index, effect) {
94
106
  const chain = [...document.chain];
95
107
  chain[index] = effect;
96
108
  return { version: 1, chain };
97
109
  }
98
110
 
99
- function validateParameterEvents(events, document, frameCount) {
111
+ function validateParameterEvents(events, document, processingDocument, frameCount) {
100
112
  if (events === undefined) return [];
101
113
  if (!Array.isArray(events)) {
102
114
  throw new ValidationError('events must be an array.');
103
115
  }
104
116
  let workingDocument = cloneDocument(document);
117
+ let workingProcessingDocument = cloneDocument(processingDocument);
118
+ const pendingPatches = new Map();
105
119
  let previousFrame = -1;
106
120
  return events.map((event, eventIndex) => {
107
121
  if (!isRecord(event)) {
@@ -123,12 +137,32 @@ function validateParameterEvents(events, document, frameCount) {
123
137
  previousFrame = event.frame;
124
138
  const index = effectIndex(workingDocument, event.effectId);
125
139
  const effect = updateEffectParameters(workingDocument.chain[index], event.parameters);
140
+ let pending = pendingPatches.get(event.effectId);
141
+ if (!pending || pending.frame !== event.frame) {
142
+ pending = {
143
+ frame: event.frame,
144
+ effect: workingProcessingDocument.chain[index],
145
+ parameters: {}
146
+ };
147
+ pendingPatches.set(event.effectId, pending);
148
+ }
149
+ Object.assign(pending.parameters, event.parameters);
150
+ const processingEffect = updateProcessingEffectParameters(
151
+ pending.effect,
152
+ pending.parameters
153
+ );
126
154
  workingDocument = replaceEffect(workingDocument, index, effect);
155
+ workingProcessingDocument = replaceEffect(
156
+ workingProcessingDocument,
157
+ index,
158
+ processingEffect
159
+ );
127
160
  return {
128
161
  frame: event.frame,
129
162
  effectId: event.effectId,
130
163
  effect,
131
- packed: packEffect(effect)
164
+ processingEffect,
165
+ packed: packEffect(processingEffect)
132
166
  };
133
167
  });
134
168
  }
@@ -142,6 +176,9 @@ class ChainStream {
142
176
  }) {
143
177
  this._initialDocument = cloneDocument(document);
144
178
  this._document = cloneDocument(document);
179
+ this._initialProcessingDocument = canonicalizeDocumentForProcessing(document);
180
+ this._processingDocument = cloneDocument(this._initialProcessingDocument);
181
+ this._pendingProcessingPatches = new Map();
145
182
  this._session = session;
146
183
  this._sampleRate = sampleRate;
147
184
  this._channels = channels;
@@ -154,7 +191,7 @@ class ChainStream {
154
191
 
155
192
  get preset() {
156
193
  this._assertOpen();
157
- return cloneDocument(this._document);
194
+ return cloneDocument(this._processingDocument);
158
195
  }
159
196
 
160
197
  get effects() {
@@ -180,9 +217,27 @@ class ChainStream {
180
217
  const index = effectIndex(this._document, effectId);
181
218
  validateStreamParameterUpdate(this._document.chain[index], parameterName);
182
219
  const effect = setEffectParameter(this._document.chain[index], parameterName, value);
183
- const packed = packEffect(effect);
220
+ let pending = this._pendingProcessingPatches.get(effectId);
221
+ if (!pending) {
222
+ pending = {
223
+ effect: this._processingDocument.chain[index],
224
+ parameters: {}
225
+ };
226
+ this._pendingProcessingPatches.set(effectId, pending);
227
+ }
228
+ pending.parameters[parameterName] = value;
229
+ const processingEffect = updateProcessingEffectParameters(
230
+ pending.effect,
231
+ pending.parameters
232
+ );
233
+ const packed = packEffect(processingEffect);
184
234
  this._session?.setPacked(effectId, packed.values, packed.hash, packed.bytes);
185
235
  this._document = replaceEffect(this._document, index, effect);
236
+ this._processingDocument = replaceEffect(
237
+ this._processingDocument,
238
+ index,
239
+ processingEffect
240
+ );
186
241
  return this;
187
242
  }
188
243
 
@@ -211,9 +266,15 @@ class ChainStream {
211
266
  `Audio has ${layout.channels} channel(s); this stream requires ${this._channels}.`
212
267
  );
213
268
  }
214
- const parameterEvents = validateParameterEvents(events, this._document, layout.frames);
269
+ const parameterEvents = validateParameterEvents(
270
+ events,
271
+ this._document,
272
+ this._processingDocument,
273
+ layout.frames
274
+ );
215
275
  const output = audio.map(channel => new Float32Array(channel));
216
276
  if (layout.frames === 0) return output;
277
+ this._pendingProcessingPatches.clear();
217
278
 
218
279
  let offset = 0;
219
280
  let eventIndex = 0;
@@ -228,6 +289,11 @@ class ChainStream {
228
289
  );
229
290
  const index = effectIndex(this._document, event.effectId);
230
291
  this._document = replaceEffect(this._document, index, event.effect);
292
+ this._processingDocument = replaceEffect(
293
+ this._processingDocument,
294
+ index,
295
+ event.processingEffect
296
+ );
231
297
  eventIndex += 1;
232
298
  }
233
299
  const nextEventFrame = parameterEvents[eventIndex]?.frame ?? layout.frames;
@@ -254,6 +320,8 @@ class ChainStream {
254
320
  this._assertOpen();
255
321
  this._session?.reset();
256
322
  this._document = cloneDocument(this._initialDocument);
323
+ this._processingDocument = cloneDocument(this._initialProcessingDocument);
324
+ this._pendingProcessingPatches.clear();
257
325
  this._processedFrames = 0;
258
326
  return this;
259
327
  }