@libraz/libsonare 1.7.2 → 1.8.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 (303) hide show
  1. package/NOTICE +178 -0
  2. package/README.md +26 -203
  3. package/dist/_chain_config.d.ts +14 -0
  4. package/dist/_chain_config.d.ts.map +1 -0
  5. package/dist/_effects_common.d.ts +6 -0
  6. package/dist/_effects_common.d.ts.map +1 -0
  7. package/dist/_feature_validation.d.ts +8 -0
  8. package/dist/_feature_validation.d.ts.map +1 -0
  9. package/dist/_fft_options.d.ts +24 -0
  10. package/dist/_fft_options.d.ts.map +1 -0
  11. package/dist/align_take.d.ts +50 -0
  12. package/dist/align_take.d.ts.map +1 -0
  13. package/dist/analysis.d.ts +29 -5840
  14. package/dist/analysis.d.ts.map +1 -0
  15. package/dist/analysis.js +873 -722
  16. package/dist/analysis.js.map +1 -1
  17. package/dist/analysis_helpers.d.ts +9 -0
  18. package/dist/analysis_helpers.d.ts.map +1 -0
  19. package/dist/audio.d.ts +163 -0
  20. package/dist/audio.d.ts.map +1 -0
  21. package/dist/clip_page_streamer.d.ts +133 -0
  22. package/dist/clip_page_streamer.d.ts.map +1 -0
  23. package/dist/codes.d.ts +44 -0
  24. package/dist/codes.d.ts.map +1 -0
  25. package/dist/effects_mastering.d.ts +23 -0
  26. package/dist/effects_mastering.d.ts.map +1 -0
  27. package/dist/effects_note_ops.d.ts +477 -0
  28. package/dist/effects_note_ops.d.ts.map +1 -0
  29. package/dist/effects_percussive.d.ts +185 -0
  30. package/dist/effects_percussive.d.ts.map +1 -0
  31. package/dist/effects_separation.d.ts +65 -0
  32. package/dist/effects_separation.d.ts.map +1 -0
  33. package/dist/effects_spectral.d.ts +28 -0
  34. package/dist/effects_spectral.d.ts.map +1 -0
  35. package/dist/effects_timepitch.d.ts +129 -0
  36. package/dist/effects_timepitch.d.ts.map +1 -0
  37. package/dist/effects_voice_change.d.ts +53 -0
  38. package/dist/effects_voice_change.d.ts.map +1 -0
  39. package/dist/errors.d.ts +51 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/feature_core.d.ts +341 -0
  42. package/dist/feature_core.d.ts.map +1 -0
  43. package/dist/feature_decompose.d.ts +278 -0
  44. package/dist/feature_decompose.d.ts.map +1 -0
  45. package/dist/feature_inverse.d.ts +128 -0
  46. package/dist/feature_inverse.d.ts.map +1 -0
  47. package/dist/feature_loudness.d.ts +66 -0
  48. package/dist/feature_loudness.d.ts.map +1 -0
  49. package/dist/feature_music.d.ts +307 -0
  50. package/dist/feature_music.d.ts.map +1 -0
  51. package/dist/feature_pitch.d.ts +108 -0
  52. package/dist/feature_pitch.d.ts.map +1 -0
  53. package/dist/feature_resample.d.ts +16 -0
  54. package/dist/feature_resample.d.ts.map +1 -0
  55. package/dist/feature_spectral.d.ts +137 -0
  56. package/dist/feature_spectral.d.ts.map +1 -0
  57. package/dist/feature_spectrogram.d.ts +198 -0
  58. package/dist/feature_spectrogram.d.ts.map +1 -0
  59. package/dist/features.d.ts +10 -0
  60. package/dist/features.d.ts.map +1 -0
  61. package/dist/hrtf/default.shrf +0 -0
  62. package/dist/index.d.ts +74 -7431
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +3758 -1362
  65. package/dist/index.js.map +1 -1
  66. package/dist/instrument_types.d.ts +517 -0
  67. package/dist/instrument_types.d.ts.map +1 -0
  68. package/dist/live_audio.d.ts +35 -0
  69. package/dist/live_audio.d.ts.map +1 -0
  70. package/dist/mastering_chain.d.ts +213 -0
  71. package/dist/mastering_chain.d.ts.map +1 -0
  72. package/dist/mastering_core.d.ts +457 -0
  73. package/dist/mastering_core.d.ts.map +1 -0
  74. package/dist/mastering_dynamics.d.ts +80 -0
  75. package/dist/mastering_dynamics.d.ts.map +1 -0
  76. package/dist/metering.d.ts +287 -0
  77. package/dist/metering.d.ts.map +1 -0
  78. package/dist/mixer.d.ts +464 -0
  79. package/dist/mixer.d.ts.map +1 -0
  80. package/dist/mixing_assistant.d.ts +62 -0
  81. package/dist/mixing_assistant.d.ts.map +1 -0
  82. package/dist/mixing_oneshot.d.ts +40 -0
  83. package/dist/mixing_oneshot.d.ts.map +1 -0
  84. package/dist/module_state.d.ts +15 -0
  85. package/dist/module_state.d.ts.map +1 -0
  86. package/dist/opfs_clip_pages.d.ts +28 -0
  87. package/dist/opfs_clip_pages.d.ts.map +1 -0
  88. package/dist/playback_renderer.d.ts +128 -0
  89. package/dist/playback_renderer.d.ts.map +1 -0
  90. package/dist/polyphony.d.ts +202 -0
  91. package/dist/polyphony.d.ts.map +1 -0
  92. package/dist/project.d.ts +8 -0
  93. package/dist/project.d.ts.map +1 -0
  94. package/dist/project_class.d.ts +562 -0
  95. package/dist/project_class.d.ts.map +1 -0
  96. package/dist/project_internal.d.ts +194 -0
  97. package/dist/project_internal.d.ts.map +1 -0
  98. package/dist/project_synth.d.ts +74 -0
  99. package/dist/project_synth.d.ts.map +1 -0
  100. package/dist/project_types.d.ts +652 -0
  101. package/dist/project_types.d.ts.map +1 -0
  102. package/dist/public_types.d.ts +185 -0
  103. package/dist/public_types.d.ts.map +1 -0
  104. package/dist/public_types_acoustic.d.ts +215 -0
  105. package/dist/public_types_acoustic.d.ts.map +1 -0
  106. package/dist/public_types_mastering.d.ts +510 -0
  107. package/dist/public_types_mastering.d.ts.map +1 -0
  108. package/dist/public_types_mixing.d.ts +436 -0
  109. package/dist/public_types_mixing.d.ts.map +1 -0
  110. package/dist/public_types_music.d.ts +619 -0
  111. package/dist/public_types_music.d.ts.map +1 -0
  112. package/dist/public_types_playback.d.ts +164 -0
  113. package/dist/public_types_playback.d.ts.map +1 -0
  114. package/dist/public_types_realtime.d.ts +174 -0
  115. package/dist/public_types_realtime.d.ts.map +1 -0
  116. package/dist/public_types_repair.d.ts +424 -0
  117. package/dist/public_types_repair.d.ts.map +1 -0
  118. package/dist/public_types_spectral.d.ts +697 -0
  119. package/dist/public_types_spectral.d.ts.map +1 -0
  120. package/dist/quick_analysis.d.ts +445 -0
  121. package/dist/quick_analysis.d.ts.map +1 -0
  122. package/dist/realtime_engine.d.ts +850 -0
  123. package/dist/realtime_engine.d.ts.map +1 -0
  124. package/dist/realtime_voice_changer.d.ts +158 -0
  125. package/dist/realtime_voice_changer.d.ts.map +1 -0
  126. package/dist/repair_dereverb.d.ts +187 -0
  127. package/dist/repair_dereverb.d.ts.map +1 -0
  128. package/dist/repair_impulsive.d.ts +186 -0
  129. package/dist/repair_impulsive.d.ts.map +1 -0
  130. package/dist/repair_noise.d.ts +239 -0
  131. package/dist/repair_noise.d.ts.map +1 -0
  132. package/dist/repair_trim.d.ts +123 -0
  133. package/dist/repair_trim.d.ts.map +1 -0
  134. package/dist/sample_bank.d.ts +84 -0
  135. package/dist/sample_bank.d.ts.map +1 -0
  136. package/dist/scale.d.ts +10 -0
  137. package/dist/scale.d.ts.map +1 -0
  138. package/dist/schemas/mixer-scene.schema.json +393 -0
  139. package/dist/schemas/playback-renderer-config.schema.json +392 -0
  140. package/dist/sonare-analysis.d.ts +8 -0
  141. package/dist/sonare-analysis.js +2 -2
  142. package/dist/sonare-analysis.wasm +0 -0
  143. package/dist/sonare.d.ts +3909 -0
  144. package/dist/sonare.js +2 -2
  145. package/dist/sonare.wasm +0 -0
  146. package/dist/stream_analyzer.d.ts +163 -0
  147. package/dist/stream_analyzer.d.ts.map +1 -0
  148. package/dist/stream_types.d.ts +214 -0
  149. package/dist/stream_types.d.ts.map +1 -0
  150. package/dist/streaming_mixing.d.ts +6 -0
  151. package/dist/streaming_mixing.d.ts.map +1 -0
  152. package/dist/streaming_processors.d.ts +335 -0
  153. package/dist/streaming_processors.d.ts.map +1 -0
  154. package/dist/transcribe.d.ts +77 -0
  155. package/dist/transcribe.d.ts.map +1 -0
  156. package/dist/validation.d.ts +140 -0
  157. package/dist/validation.d.ts.map +1 -0
  158. package/dist/web_midi.d.ts +77 -0
  159. package/dist/web_midi.d.ts.map +1 -0
  160. package/dist/worker.d.ts +5 -48
  161. package/dist/worker.d.ts.map +1 -0
  162. package/dist/worker.js +94 -41
  163. package/dist/worker.js.map +1 -1
  164. package/dist/worker_client.d.ts +96 -0
  165. package/dist/worker_client.d.ts.map +1 -0
  166. package/dist/worker_protocol.d.ts +43 -0
  167. package/dist/worker_protocol.d.ts.map +1 -0
  168. package/dist/worklet/audio_types.d.ts +21 -0
  169. package/dist/worklet/audio_types.d.ts.map +1 -0
  170. package/dist/worklet/engine-automation.d.ts +29 -0
  171. package/dist/worklet/engine-automation.d.ts.map +1 -0
  172. package/dist/worklet/engine-capture-facade.d.ts +35 -0
  173. package/dist/worklet/engine-capture-facade.d.ts.map +1 -0
  174. package/dist/worklet/engine-clips.d.ts +23 -0
  175. package/dist/worklet/engine-clips.d.ts.map +1 -0
  176. package/dist/worklet/engine-markers.d.ts +40 -0
  177. package/dist/worklet/engine-markers.d.ts.map +1 -0
  178. package/dist/worklet/engine-mixer-facade.d.ts +128 -0
  179. package/dist/worklet/engine-mixer-facade.d.ts.map +1 -0
  180. package/dist/worklet/engine-node.d.ts +81 -0
  181. package/dist/worklet/engine-node.d.ts.map +1 -0
  182. package/dist/worklet/engine-offline.d.ts +81 -0
  183. package/dist/worklet/engine-offline.d.ts.map +1 -0
  184. package/dist/worklet/engine-options.d.ts +12 -0
  185. package/dist/worklet/engine-options.d.ts.map +1 -0
  186. package/dist/worklet/engine-parameter-facade.d.ts +106 -0
  187. package/dist/worklet/engine-parameter-facade.d.ts.map +1 -0
  188. package/dist/worklet/engine-processor.d.ts +68 -0
  189. package/dist/worklet/engine-processor.d.ts.map +1 -0
  190. package/dist/worklet/engine-register.d.ts +2 -0
  191. package/dist/worklet/engine-register.d.ts.map +1 -0
  192. package/dist/worklet/engine-strips.d.ts +73 -0
  193. package/dist/worklet/engine-strips.d.ts.map +1 -0
  194. package/dist/worklet/engine-sync.d.ts +37 -0
  195. package/dist/worklet/engine-sync.d.ts.map +1 -0
  196. package/dist/worklet/engine-tempo-facade.d.ts +48 -0
  197. package/dist/worklet/engine-tempo-facade.d.ts.map +1 -0
  198. package/dist/worklet/engine.d.ts +397 -0
  199. package/dist/worklet/engine.d.ts.map +1 -0
  200. package/dist/worklet/guards.d.ts +47 -0
  201. package/dist/worklet/guards.d.ts.map +1 -0
  202. package/dist/worklet/messages.d.ts +710 -0
  203. package/dist/worklet/messages.d.ts.map +1 -0
  204. package/dist/worklet/mixer-processor.d.ts +46 -0
  205. package/dist/worklet/mixer-processor.d.ts.map +1 -0
  206. package/dist/worklet/playback-processor.d.ts +62 -0
  207. package/dist/worklet/playback-processor.d.ts.map +1 -0
  208. package/dist/worklet/protocol.d.ts +323 -0
  209. package/dist/worklet/protocol.d.ts.map +1 -0
  210. package/dist/worklet/voice-changer-processor.d.ts +41 -0
  211. package/dist/worklet/voice-changer-processor.d.ts.map +1 -0
  212. package/dist/worklet.d.ts +16 -2515
  213. package/dist/worklet.d.ts.map +1 -0
  214. package/dist/worklet.js +2763 -501
  215. package/dist/worklet.js.map +1 -1
  216. package/package.json +23 -12
  217. package/src/_effects_common.ts +17 -0
  218. package/src/_feature_validation.ts +34 -0
  219. package/src/_fft_options.ts +39 -0
  220. package/src/align_take.ts +64 -0
  221. package/src/analysis.ts +56 -3
  222. package/src/analysis_helpers.ts +7 -0
  223. package/src/audio.ts +106 -3
  224. package/src/codes.ts +39 -2
  225. package/src/effects_mastering.ts +97 -22
  226. package/src/effects_note_ops.ts +635 -0
  227. package/src/effects_percussive.ts +217 -0
  228. package/src/effects_separation.ts +150 -0
  229. package/src/effects_spectral.ts +60 -0
  230. package/src/effects_timepitch.ts +377 -0
  231. package/src/errors.ts +23 -1
  232. package/src/feature_core.ts +127 -2
  233. package/src/feature_decompose.ts +633 -0
  234. package/src/feature_inverse.ts +454 -0
  235. package/src/feature_loudness.ts +125 -0
  236. package/src/feature_music.ts +107 -14
  237. package/src/feature_pitch.ts +96 -1
  238. package/src/feature_spectral.ts +16 -611
  239. package/src/feature_spectrogram.ts +63 -450
  240. package/src/features.ts +36 -22
  241. package/src/index.ts +282 -30
  242. package/src/instrument_types.ts +645 -0
  243. package/src/live_audio.ts +27 -1
  244. package/src/mastering_chain.ts +184 -0
  245. package/src/mastering_core.ts +346 -32
  246. package/src/mastering_dynamics.ts +22 -11
  247. package/src/metering.ts +67 -24
  248. package/src/mixer.ts +212 -3
  249. package/src/mixing_assistant.ts +138 -0
  250. package/src/mixing_oneshot.ts +10 -5
  251. package/src/module_state.ts +24 -2
  252. package/src/playback_renderer.ts +252 -0
  253. package/src/polyphony.ts +279 -0
  254. package/src/project.ts +61 -24
  255. package/src/project_class.ts +450 -27
  256. package/src/project_internal.ts +149 -42
  257. package/src/project_synth.ts +67 -1
  258. package/src/project_types.ts +268 -270
  259. package/src/public_types.ts +122 -3
  260. package/src/public_types_acoustic.ts +112 -3
  261. package/src/public_types_mastering.ts +254 -73
  262. package/src/public_types_mixing.ts +363 -1
  263. package/src/public_types_music.ts +312 -2
  264. package/src/public_types_playback.ts +195 -0
  265. package/src/public_types_realtime.ts +39 -7
  266. package/src/public_types_repair.ts +446 -0
  267. package/src/public_types_spectral.ts +487 -1
  268. package/src/quick_analysis.ts +203 -26
  269. package/src/realtime_engine.ts +711 -28
  270. package/src/realtime_voice_changer.ts +55 -1
  271. package/src/repair_dereverb.ts +299 -0
  272. package/src/repair_impulsive.ts +395 -0
  273. package/src/repair_noise.ts +425 -0
  274. package/src/repair_trim.ts +226 -0
  275. package/src/sample_bank.ts +113 -0
  276. package/src/sonare.js.d.ts +1122 -30
  277. package/src/stream_analyzer.ts +36 -4
  278. package/src/stream_types.ts +37 -0
  279. package/src/streaming_mixing.ts +1 -1
  280. package/src/streaming_processors.ts +194 -10
  281. package/src/transcribe.ts +89 -0
  282. package/src/validation.ts +285 -11
  283. package/src/web_midi.ts +1 -6
  284. package/src/worker.ts +18 -2
  285. package/src/worklet/audio_types.ts +37 -0
  286. package/src/worklet/engine-mixer-facade.ts +444 -10
  287. package/src/worklet/engine-node.ts +78 -26
  288. package/src/worklet/engine-offline.ts +14 -8
  289. package/src/worklet/engine-parameter-facade.ts +21 -0
  290. package/src/worklet/engine-processor.ts +272 -83
  291. package/src/worklet/engine-register.ts +32 -18
  292. package/src/worklet/engine-strips.ts +280 -9
  293. package/src/worklet/engine-sync.ts +14 -6
  294. package/src/worklet/engine.ts +365 -31
  295. package/src/worklet/guards.ts +140 -44
  296. package/src/worklet/messages.ts +227 -2
  297. package/src/worklet/mixer-processor.ts +109 -47
  298. package/src/worklet/playback-processor.ts +300 -0
  299. package/src/worklet/protocol.ts +53 -3
  300. package/src/worklet/voice-changer-processor.ts +17 -11
  301. package/src/worklet.ts +17 -0
  302. package/src/effects_transform.ts +0 -718
  303. package/src/mastering_repair.ts +0 -273
@@ -1,6 +1,8 @@
1
1
  import type { EngineClip } from '../index';
2
2
  import { RealtimeEngine } from '../index';
3
- import type { WorkletInput, WorkletOutput } from './audio_types';
3
+ import { copyPlanesToOutput, type WorkletInput, type WorkletOutput } from './audio_types';
4
+ import { emptyStripJson } from './engine-mixer-facade';
5
+ import { requireChannelCount, requireIntegerOption } from './guards';
4
6
  import {
5
7
  DEFAULT_METRONOME_CONFIG,
6
8
  type ResolvedMetronomeConfig,
@@ -39,37 +41,12 @@ import {
39
41
  type SonareEngineTelemetryRecord,
40
42
  type SonareEngineTelemetryRingBuffer,
41
43
  SonareEngineTelemetryType,
42
- type SonareWorkletMeterSnapshot,
43
44
  scopeRingFromSharedBuffer,
44
45
  telemetryFromEngine,
45
46
  writeInt64Words,
46
47
  writeSonareEngineTelemetryRingBuffer,
47
48
  } from './protocol';
48
49
 
49
- /**
50
- * Copies one plane per output channel, zero-filling the tail past `frames` and
51
- * any channel the source does not cover. Shared by the program and cue outputs
52
- * so the two cannot drift in their padding behaviour. Allocation-free.
53
- */
54
- function copyPlanesToOutput(
55
- output: Float32Array[],
56
- planes: readonly Float32Array[],
57
- frames: number,
58
- ): void {
59
- for (let ch = 0; ch < output.length; ch++) {
60
- const target = output[ch];
61
- const source = planes[ch] ?? planes[0];
62
- if (source) {
63
- target.set(source.subarray(0, Math.min(target.length, frames)));
64
- if (target.length > frames) {
65
- target.fill(0, frames);
66
- }
67
- } else {
68
- target.fill(0);
69
- }
70
- }
71
- }
72
-
73
50
  function captureTransferList(channels: readonly Float32Array[]): Transferable[] {
74
51
  const transfers: ArrayBuffer[] = [];
75
52
  const seen = new Set<ArrayBuffer>();
@@ -141,9 +118,15 @@ export class SonareRealtimeEngineWorkletProcessor {
141
118
  ) {
142
119
  this.sampleRate = options.sampleRate ?? 48000;
143
120
  this.blockSize = options.blockSize ?? 128;
144
- this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
121
+ this.channelCount = requireChannelCount(options.channelCount, 2);
145
122
  this.transport = transport;
146
- this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
123
+ // Zero is the off switch for meter publication, so the floor is 0.
124
+ this.meterIntervalFrames = requireIntegerOption(
125
+ options.meterIntervalFrames,
126
+ 2048,
127
+ 'meterIntervalFrames',
128
+ 0,
129
+ );
147
130
  this.commandRing = options.commandSharedBuffer
148
131
  ? this.commandRingFromSharedBuffer(options.commandSharedBuffer, options.commandRingCapacity)
149
132
  : undefined;
@@ -175,10 +158,13 @@ export class SonareRealtimeEngineWorkletProcessor {
175
158
  options.externalMidiRingCapacity,
176
159
  )
177
160
  : undefined;
161
+ if (this.commandRing && this.commandRing.capacity > 65536) {
162
+ throw new RangeError('commandRingCapacity must be at most 65536');
163
+ }
178
164
  this.engine = new RealtimeEngine(
179
165
  this.sampleRate,
180
166
  this.blockSize,
181
- 1024,
167
+ Math.max(1024, this.commandRing?.capacity ?? 0),
182
168
  1024,
183
169
  this.channelCount,
184
170
  );
@@ -201,7 +187,13 @@ export class SonareRealtimeEngineWorkletProcessor {
201
187
  // band count follows the ring's record layout so writeScopeRing never
202
188
  // overruns its slot.
203
189
  if (this.scopeRing) {
204
- const interval = Math.max(1, Math.floor(options.scopeIntervalFrames ?? this.blockSize));
190
+ // Zero is the engine's own off switch for the scope tap, so the floor is 0.
191
+ const interval = requireIntegerOption(
192
+ options.scopeIntervalFrames,
193
+ this.blockSize,
194
+ 'scopeIntervalFrames',
195
+ 0,
196
+ );
205
197
  this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
206
198
  }
207
199
  }
@@ -395,6 +387,14 @@ export class SonareRealtimeEngineWorkletProcessor {
395
387
  }
396
388
  break;
397
389
  }
390
+ case 'syncClipPagePrefetchFrames': {
391
+ this.engine.setClipPagePrefetchFrames(message.frames);
392
+ break;
393
+ }
394
+ case 'syncWarpVoiceCapacity': {
395
+ this.engine.setWarpVoiceCapacity(message.voices);
396
+ break;
397
+ }
398
398
  case 'syncClipPageCommit': {
399
399
  const providerId = this.pagedClipProviders.get(message.clipId);
400
400
  const clip = message.clip ?? this.pendingPagedClips.get(message.clipId);
@@ -455,11 +455,69 @@ export class SonareRealtimeEngineWorkletProcessor {
455
455
  );
456
456
  }
457
457
  break;
458
- case 'syncMixer':
458
+ case 'syncMixer': {
459
+ if (message.sidechainDelta === true) {
460
+ // Sidechain setters send a binding delta through this message type.
461
+ // Their `lanes` field is context, not a request to rebuild routing;
462
+ // rebuilding it here would discard live insert smoother slots.
463
+ for (const binding of message.laneSidechains ?? []) {
464
+ this.engine.setLaneSidechain(
465
+ binding.trackId,
466
+ binding.insertIndex,
467
+ binding.sourceTrackId,
468
+ );
469
+ }
470
+ for (const binding of message.busSidechains ?? []) {
471
+ this.engine.setBusSidechain(
472
+ binding.busId,
473
+ binding.insertIndex,
474
+ binding.sourceKind,
475
+ binding.sourceId,
476
+ );
477
+ }
478
+ for (const binding of message.masterSidechains ?? []) {
479
+ this.engine.setMasterSidechain(
480
+ binding.insertIndex,
481
+ binding.sourceKind,
482
+ binding.sourceId,
483
+ );
484
+ }
485
+ break;
486
+ }
487
+ // SAB commands are published before the control thread posts this
488
+ // structural sync. Drain the whole snapshot so a future command past
489
+ // the normal 64-per-render budget cannot enter native after the strip
490
+ // replacement has invalidated its target.
491
+ if (this.commandRing) {
492
+ const write = Atomics.load(this.commandRing.header, 0);
493
+ const read = Atomics.load(this.commandRing.header, 1);
494
+ this.drainCommands(Math.min(this.commandRing.capacity, Math.max(0, write - read)));
495
+ }
496
+ // Complete earlier immediate edits in message order. Scheduled
497
+ // commands remain queued for their render frame.
498
+ this.engine.applyCommandsDueNowPreservingFuture();
499
+ this.engine.settleInsertParameters();
459
500
  if (message.buses) {
460
501
  this.engine.setTrackBuses(message.buses);
461
502
  }
462
503
  this.engine.setTrackLanes(message.lanes);
504
+ for (const target of message.forceInsertResets ?? []) {
505
+ const emptyJson = emptyStripJson(target);
506
+ switch (target.kind) {
507
+ case 'track':
508
+ this.engine.clearTrackInsertParameterBases(target.trackId);
509
+ this.engine.setTrackStripJson(target.trackId, emptyJson);
510
+ break;
511
+ case 'bus':
512
+ this.engine.clearBusInsertParameterBases(target.busId);
513
+ this.engine.setBusStripJson(target.busId, emptyJson);
514
+ break;
515
+ case 'master':
516
+ this.engine.clearMasterInsertParameterBases();
517
+ this.engine.setMasterStripJson(emptyJson);
518
+ break;
519
+ }
520
+ }
463
521
  for (const strip of message.trackStrips ?? []) {
464
522
  this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
465
523
  }
@@ -472,7 +530,46 @@ export class SonareRealtimeEngineWorkletProcessor {
472
530
  for (const binding of message.laneSidechains ?? []) {
473
531
  this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
474
532
  }
533
+ for (const binding of message.busSidechains ?? []) {
534
+ this.engine.setBusSidechain(
535
+ binding.busId,
536
+ binding.insertIndex,
537
+ binding.sourceKind,
538
+ binding.sourceId,
539
+ );
540
+ }
541
+ for (const binding of message.masterSidechains ?? []) {
542
+ this.engine.setMasterSidechain(binding.insertIndex, binding.sourceKind, binding.sourceId);
543
+ }
544
+ for (const override of message.insertParamOverrides ?? []) {
545
+ switch (override.kind) {
546
+ case 'track':
547
+ this.engine.restoreTrackStripInsertParamByName(
548
+ override.trackId,
549
+ override.insertIndex,
550
+ override.paramName,
551
+ override.value,
552
+ );
553
+ break;
554
+ case 'bus':
555
+ this.engine.restoreBusStripInsertParamByName(
556
+ override.busId,
557
+ override.insertIndex,
558
+ override.paramName,
559
+ override.value,
560
+ );
561
+ break;
562
+ case 'master':
563
+ this.engine.restoreMasterStripInsertParamByName(
564
+ override.insertIndex,
565
+ override.paramName,
566
+ override.value,
567
+ );
568
+ break;
569
+ }
570
+ }
475
571
  break;
572
+ }
476
573
  case 'syncCapture':
477
574
  this.engine.setCaptureBuffer(message.channels, message.bufferFrames);
478
575
  this.engine.setCaptureSource(message.source);
@@ -485,6 +582,9 @@ export class SonareRealtimeEngineWorkletProcessor {
485
582
  case 'syncMasterStripEqBand':
486
583
  this.engine.setMasterStripEqBandJson(message.bandIndex, message.bandJson);
487
584
  break;
585
+ case 'syncBusStripEqBand':
586
+ this.engine.setBusStripEqBandJson(message.busId, message.bandIndex, message.bandJson);
587
+ break;
488
588
  case 'syncTrackStripInsertBypassed':
489
589
  this.engine.setTrackStripInsertBypassed(
490
590
  message.trackId,
@@ -501,26 +601,65 @@ export class SonareRealtimeEngineWorkletProcessor {
501
601
  );
502
602
  break;
503
603
  case 'syncTrackStripInsertParamByName':
504
- this.engine.setTrackStripInsertParamByName(
505
- message.trackId,
506
- message.insertIndex,
507
- message.paramName,
508
- message.value,
509
- );
604
+ this.engine.applyCommandsDueNowPreservingFuture();
605
+ if (
606
+ !this.engine.applyTrackStripInsertParamByNameNow(
607
+ message.trackId,
608
+ message.insertIndex,
609
+ message.paramName,
610
+ message.value,
611
+ )
612
+ ) {
613
+ // A burst can exhaust the fixed smoother slots before the next
614
+ // render quantum. Apply the last knob value exactly in that case.
615
+ this.engine.restoreTrackStripInsertParamByName(
616
+ message.trackId,
617
+ message.insertIndex,
618
+ message.paramName,
619
+ message.value,
620
+ );
621
+ }
510
622
  break;
511
623
  case 'syncMasterStripInsertParamByName':
512
- this.engine.setMasterStripInsertParamByName(
513
- message.insertIndex,
514
- message.paramName,
515
- message.value,
516
- );
624
+ this.engine.applyCommandsDueNowPreservingFuture();
625
+ if (
626
+ !this.engine.applyMasterStripInsertParamByNameNow(
627
+ message.insertIndex,
628
+ message.paramName,
629
+ message.value,
630
+ )
631
+ ) {
632
+ this.engine.restoreMasterStripInsertParamByName(
633
+ message.insertIndex,
634
+ message.paramName,
635
+ message.value,
636
+ );
637
+ }
517
638
  break;
518
639
  case 'syncBusStripInsertParamByName':
519
- this.engine.setBusStripInsertParamByName(
640
+ this.engine.applyCommandsDueNowPreservingFuture();
641
+ if (
642
+ !this.engine.applyBusStripInsertParamByNameNow(
643
+ message.busId,
644
+ message.insertIndex,
645
+ message.paramName,
646
+ message.value,
647
+ )
648
+ ) {
649
+ this.engine.restoreBusStripInsertParamByName(
650
+ message.busId,
651
+ message.insertIndex,
652
+ message.paramName,
653
+ message.value,
654
+ );
655
+ }
656
+ break;
657
+ case 'syncBusStripInsertBypassed':
658
+ this.engine.setBusStripInsertBypassed(
520
659
  message.busId,
521
660
  message.insertIndex,
522
- message.paramName,
523
- message.value,
661
+ message.bypassed,
662
+ message.resetOnBypass,
524
663
  );
525
664
  break;
526
665
  case 'syncTrackStripPan':
@@ -535,6 +674,18 @@ export class SonareRealtimeEngineWorkletProcessor {
535
674
  case 'syncTrackStripDualPan':
536
675
  this.engine.setTrackStripDualPan(message.trackId, message.leftPan, message.rightPan);
537
676
  break;
677
+ case 'syncBusStripPan':
678
+ this.engine.setBusStripPan(message.busId, message.pan);
679
+ break;
680
+ case 'syncBusStripPanLaw':
681
+ this.engine.setBusStripPanLaw(message.busId, message.panLaw);
682
+ break;
683
+ case 'syncBusStripPanMode':
684
+ this.engine.setBusStripPanMode(message.busId, message.panMode);
685
+ break;
686
+ case 'syncBusStripDualPan':
687
+ this.engine.setBusStripDualPan(message.busId, message.leftPan, message.rightPan);
688
+ break;
538
689
  case 'syncTrackStripChannelDelaySamples':
539
690
  this.engine.setTrackStripChannelDelaySamples(message.trackId, message.delaySamples);
540
691
  break;
@@ -586,8 +737,36 @@ export class SonareRealtimeEngineWorkletProcessor {
586
737
  message.renderFrame,
587
738
  );
588
739
  break;
740
+ case 'syncMidiPitchBend':
741
+ this.engine.pushMidiPitchBend(
742
+ message.destinationId,
743
+ message.group,
744
+ message.channel,
745
+ message.data0,
746
+ message.renderFrame,
747
+ );
748
+ break;
749
+ case 'syncMidiChannelPressure':
750
+ this.engine.pushMidiChannelPressure(
751
+ message.destinationId,
752
+ message.group,
753
+ message.channel,
754
+ message.data0,
755
+ message.renderFrame,
756
+ );
757
+ break;
758
+ case 'syncMidiPolyPressure':
759
+ this.engine.pushMidiPolyPressure(
760
+ message.destinationId,
761
+ message.group,
762
+ message.channel,
763
+ message.data0,
764
+ message.data1,
765
+ message.renderFrame,
766
+ );
767
+ break;
589
768
  case 'syncMidiUmp':
590
- this.engine.pushMidiUmp(message.destinationId, message.word0, message.renderFrame);
769
+ this.engine.pushMidiUmp(message.destinationId, [message.word0], message.renderFrame);
591
770
  break;
592
771
  case 'syncMidiSysex':
593
772
  this.engine.pushMidiSysex(message.destinationId, message.data, message.renderFrame);
@@ -640,6 +819,31 @@ export class SonareRealtimeEngineWorkletProcessor {
640
819
  message.portTimeSamples,
641
820
  );
642
821
  break;
822
+ case 'syncMidiInputPitchBend':
823
+ this.engine.pushMidiInputPitchBend(
824
+ message.group,
825
+ message.channel,
826
+ message.data0,
827
+ message.portTimeSamples,
828
+ );
829
+ break;
830
+ case 'syncMidiInputChannelPressure':
831
+ this.engine.pushMidiInputChannelPressure(
832
+ message.group,
833
+ message.channel,
834
+ message.data0,
835
+ message.portTimeSamples,
836
+ );
837
+ break;
838
+ case 'syncMidiInputPolyPressure':
839
+ this.engine.pushMidiInputPolyPressure(
840
+ message.group,
841
+ message.channel,
842
+ message.data0,
843
+ message.data1,
844
+ message.portTimeSamples,
845
+ );
846
+ break;
643
847
  }
644
848
  }
645
849
 
@@ -727,11 +931,11 @@ export class SonareRealtimeEngineWorkletProcessor {
727
931
  }
728
932
  }
729
933
 
730
- private drainCommands(): void {
934
+ private drainCommands(limit = 64): void {
731
935
  if (!this.commandRing) {
732
936
  return;
733
937
  }
734
- for (let i = 0; i < 64; i++) {
938
+ for (let i = 0; i < limit; i++) {
735
939
  const command = popSonareEngineCommandRingBuffer(this.commandRing);
736
940
  if (!command) {
737
941
  return;
@@ -947,14 +1151,15 @@ export class SonareRealtimeEngineWorkletProcessor {
947
1151
  if (meter.frame !== this.lastMeterFrame) {
948
1152
  this.lastMeterFrame = meter.frame;
949
1153
  }
950
- // Prefer the lock-free SAB meter ring (matching the telemetry path and
951
- // SonareWorkletProcessor); only fall back to structured-clone postMessage
952
- // when no ring was provided, so we do not allocate/post from the audio
953
- // render callback in SAB mode.
954
- if (this.meterRing) {
955
- this.writeMeterRing(meter);
1154
+ // The ring branch returned above, so this is the structured-clone
1155
+ // fallback. `onMeter` and `postMessage` are alternative channels for the
1156
+ // same record, not a broadcast pair: the AudioWorklet registration
1157
+ // resolves BOTH to `port.postMessage`, so calling them in turn delivered
1158
+ // every record twice and doubled the host's peak-hold decay rate. Take
1159
+ // the first channel the transport offers.
1160
+ if (this.transport?.onMeter) {
1161
+ this.transport.onMeter(meter);
956
1162
  } else {
957
- this.transport?.onMeter?.(meter);
958
1163
  this.transport?.postMessage?.(meter);
959
1164
  }
960
1165
  }
@@ -973,34 +1178,18 @@ export class SonareRealtimeEngineWorkletProcessor {
973
1178
  Atomics.store(ring.header, 0, writeIndex + 1);
974
1179
  }
975
1180
 
976
- private writeMeterRing(meter: SonareWorkletMeterSnapshot): void {
977
- const ring = this.meterRing;
978
- if (!ring) {
979
- return;
980
- }
981
- const writeIndex = Atomics.load(ring.header, 0);
982
- const offset = (writeIndex % ring.capacity) * SONARE_METER_RING_RECORD_FLOATS;
983
- ring.records[offset] = encodeFrameLo(meter.frame);
984
- ring.records[offset + 1] = encodeFrameHi(meter.frame);
985
- ring.records[offset + 2] = meter.targetId;
986
- ring.records[offset + 3] = meter.peakDbL;
987
- ring.records[offset + 4] = meter.peakDbR;
988
- ring.records[offset + 5] = meter.rmsDbL;
989
- ring.records[offset + 6] = meter.rmsDbR;
990
- ring.records[offset + 7] = meter.correlation;
991
- ring.records[offset + 8] = meter.truePeakDbL;
992
- ring.records[offset + 9] = meter.truePeakDbR;
993
- ring.records[offset + 10] = meter.momentaryLufs;
994
- ring.records[offset + 11] = meter.shortTermLufs;
995
- ring.records[offset + 12] = meter.integratedLufs;
996
- ring.records[offset + 13] = meter.gainReductionDb;
997
- Atomics.store(ring.header, 0, writeIndex + 1);
998
- // writeIndex is a free-running monotonic counter, so an overflow guard here
999
- // would fire on essentially every write past the first `capacity` records
1000
- // and store an ever-growing value, not a dropped-record count. Readers
1001
- // already detect silent overrun via firstReadable = max(readIndex,
1002
- // writeIndex - capacity), so header slot 3 is left at its initial 0.
1003
- }
1181
+ // A snapshot-object ring writer used to live here as well. It was
1182
+ // unreachable: publishMeters returns after the writeMeterScratch loop
1183
+ // whenever a ring exists, so the ring is never fed from a materialised
1184
+ // snapshot. writeMeterScratch above is the only ring writer, and it keeps the
1185
+ // render callback allocation-free by reading the engine's scratch registers
1186
+ // directly.
1187
+ //
1188
+ // writeIndex is a free-running monotonic counter, so an overflow guard would
1189
+ // fire on essentially every write past the first `capacity` records and store
1190
+ // an ever-growing value, not a dropped-record count. Readers already detect
1191
+ // silent overrun via firstReadable = max(readIndex, writeIndex - capacity),
1192
+ // so header slot 3 is left at its initial 0.
1004
1193
 
1005
1194
  // Drains the engine's scope producer (FFT spectrum + goniometer points) into
1006
1195
  // the lock-free SAB scope ring. No allocation on the render path: records are
@@ -9,6 +9,7 @@ import {
9
9
  isEngineTransportRequestMessage,
10
10
  } from './guards';
11
11
  import type { SonareRealtimeEngineWorkletProcessorOptions, WorkletPort } from './messages';
12
+ import { isRecord } from './protocol';
12
13
 
13
14
  export function registerSonareRealtimeEngineWorkletProcessor(
14
15
  name = 'sonare-realtime-engine-processor',
@@ -43,15 +44,7 @@ export function registerSonareRealtimeEngineWorkletProcessor(
43
44
  }
44
45
  return;
45
46
  }
46
- if (isEngineCommandRecord(event.data)) {
47
- this.bridge.receiveCommand(event.data);
48
- } else if (isEngineSyncMessage(event.data)) {
49
- this.bridge.receiveSync(event.data);
50
- } else if (isEngineCaptureRequestMessage(event.data)) {
51
- this.bridge.receiveCaptureRequest(event.data);
52
- } else if (isEngineTransportRequestMessage(event.data)) {
53
- this.bridge.receiveTransportRequest(event.data);
54
- }
47
+ this.routeMessage(event.data);
55
48
  };
56
49
  if (port?.addEventListener) {
57
50
  port.addEventListener('message', onMessage);
@@ -72,18 +65,39 @@ export function registerSonareRealtimeEngineWorkletProcessor(
72
65
  return true;
73
66
  }
74
67
 
68
+ /**
69
+ * Single dispatch point for the port, shared with the buffered replay so
70
+ * the two cannot recognize different message sets.
71
+ */
72
+ private routeMessage(data: unknown): void {
73
+ const bridge = this.bridge;
74
+ if (!bridge) {
75
+ return;
76
+ }
77
+ if (isEngineCommandRecord(data)) {
78
+ bridge.receiveCommand(data);
79
+ } else if (isEngineSyncMessage(data)) {
80
+ bridge.receiveSync(data);
81
+ } else if (isEngineCaptureRequestMessage(data)) {
82
+ bridge.receiveCaptureRequest(data);
83
+ } else if (isEngineTransportRequestMessage(data)) {
84
+ bridge.receiveTransportRequest(data);
85
+ } else if (isRecord(data) && typeof data.type === 'string' && data.type.startsWith('sync')) {
86
+ // A sync the guard does not accept would otherwise vanish, leaving the
87
+ // live engine out of step with the offline mirror and nothing to see.
88
+ // Report it through the same channel as a sync the engine rejected.
89
+ this.port?.postMessage?.({
90
+ type: 'syncError',
91
+ syncType: data.type,
92
+ message: `Unrecognized worklet sync message: ${data.type}`,
93
+ });
94
+ }
95
+ }
96
+
75
97
  private replayPendingMessages(): void {
76
98
  const messages = this.pendingMessages.splice(0);
77
99
  for (const data of messages) {
78
- if (isEngineCommandRecord(data)) {
79
- this.bridge?.receiveCommand(data);
80
- } else if (isEngineSyncMessage(data)) {
81
- this.bridge?.receiveSync(data);
82
- } else if (isEngineCaptureRequestMessage(data)) {
83
- this.bridge?.receiveCaptureRequest(data);
84
- } else if (isEngineTransportRequestMessage(data)) {
85
- this.bridge?.receiveTransportRequest(data);
86
- }
100
+ this.routeMessage(data);
87
101
  }
88
102
  }
89
103