@libraz/libsonare 1.7.1 → 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 -170
  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 -5837
  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 -7329
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +3816 -1359
  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 -2142
  213. package/dist/worklet.d.ts.map +1 -0
  214. package/dist/worklet.js +2889 -466
  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 +285 -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 +64 -24
  255. package/src/project_class.ts +502 -29
  256. package/src/project_internal.ts +151 -42
  257. package/src/project_synth.ts +67 -1
  258. package/src/project_types.ts +302 -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 +750 -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 +1125 -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 +212 -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 +85 -28
  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 +307 -71
  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 +239 -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 +22 -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,7 +41,6 @@ import {
39
41
  type SonareEngineTelemetryRecord,
40
42
  type SonareEngineTelemetryRingBuffer,
41
43
  SonareEngineTelemetryType,
42
- type SonareWorkletMeterSnapshot,
43
44
  scopeRingFromSharedBuffer,
44
45
  telemetryFromEngine,
45
46
  writeInt64Words,
@@ -94,6 +95,11 @@ export class SonareRealtimeEngineWorkletProcessor {
94
95
  // allocated per render quantum (the old engine.process() round-tripped fresh
95
96
  // arrays on both heaps every block, an RT-safety hazard).
96
97
  private channelBuffers: Float32Array[];
98
+ // Cue-bus plane, allocated only when the host asked for a separate PFL/AFL
99
+ // output. Empty otherwise, so a single-output host pays no heap and keeps the
100
+ // historical behaviour where process() folds the cue into the program mix.
101
+ private monitorBuffers: Float32Array[] = [];
102
+ private readonly cueOutput: boolean;
97
103
  private readonly liveClips = new Map<number, EngineClip>();
98
104
  private readonly pagedClipProviders = new Map<number, number>();
99
105
  private readonly pagedClipPageFrames = new Map<number, number>();
@@ -112,9 +118,15 @@ export class SonareRealtimeEngineWorkletProcessor {
112
118
  ) {
113
119
  this.sampleRate = options.sampleRate ?? 48000;
114
120
  this.blockSize = options.blockSize ?? 128;
115
- this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
121
+ this.channelCount = requireChannelCount(options.channelCount, 2);
116
122
  this.transport = transport;
117
- 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
+ );
118
130
  this.commandRing = options.commandSharedBuffer
119
131
  ? this.commandRingFromSharedBuffer(options.commandSharedBuffer, options.commandRingCapacity)
120
132
  : undefined;
@@ -146,10 +158,13 @@ export class SonareRealtimeEngineWorkletProcessor {
146
158
  options.externalMidiRingCapacity,
147
159
  )
148
160
  : undefined;
161
+ if (this.commandRing && this.commandRing.capacity > 65536) {
162
+ throw new RangeError('commandRingCapacity must be at most 65536');
163
+ }
149
164
  this.engine = new RealtimeEngine(
150
165
  this.sampleRate,
151
166
  this.blockSize,
152
- 1024,
167
+ Math.max(1024, this.commandRing?.capacity ?? 0),
153
168
  1024,
154
169
  this.channelCount,
155
170
  );
@@ -160,11 +175,25 @@ export class SonareRealtimeEngineWorkletProcessor {
160
175
  for (let ch = 0; ch < this.channelCount; ch++) {
161
176
  this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
162
177
  }
178
+ this.cueOutput = options.cueOutput === true;
179
+ if (this.cueOutput) {
180
+ this.engine.prepareMonitorChannels(this.channelCount, this.blockSize);
181
+ this.monitorBuffers = new Array(this.channelCount);
182
+ for (let ch = 0; ch < this.channelCount; ch++) {
183
+ this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
184
+ }
185
+ }
163
186
  // Arm the engine's scope producer only when a scope ring was provided. The
164
187
  // band count follows the ring's record layout so writeScopeRing never
165
188
  // overruns its slot.
166
189
  if (this.scopeRing) {
167
- 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
+ );
168
197
  this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
169
198
  }
170
199
  }
@@ -213,6 +242,9 @@ export class SonareRealtimeEngineWorkletProcessor {
213
242
  if ((this.channelBuffers[0]?.byteLength ?? 0) === 0) {
214
243
  this.reacquireChannelBuffers();
215
244
  }
245
+ if (this.cueOutput && (this.monitorBuffers[0]?.byteLength ?? 0) === 0) {
246
+ this.reacquireMonitorBuffers();
247
+ }
216
248
 
217
249
  const input = inputs[0];
218
250
  // Write the AudioWorklet input straight into the engine's WASM-heap views;
@@ -227,19 +259,20 @@ export class SonareRealtimeEngineWorkletProcessor {
227
259
  }
228
260
  }
229
261
 
230
- // Run the engine in place over the prepared scratch (allocation-free).
231
- this.engine.processPrepared(usableFrames);
262
+ // Run the engine in place over the prepared scratch (allocation-free). The
263
+ // monitor variant keeps the cue bus out of the program planes so it can go
264
+ // to its own output; the plain call folds it in, as it always has.
265
+ if (this.cueOutput) {
266
+ this.engine.processPreparedWithMonitor(usableFrames);
267
+ } else {
268
+ this.engine.processPrepared(usableFrames);
269
+ }
232
270
 
233
- for (let ch = 0; ch < output.length; ch++) {
234
- const target = output[ch];
235
- const source = this.channelBuffers[ch] ?? this.channelBuffers[0];
236
- if (source) {
237
- target.set(source.subarray(0, Math.min(target.length, usableFrames)));
238
- if (target.length > usableFrames) {
239
- target.fill(0, usableFrames);
240
- }
241
- } else {
242
- target.fill(0);
271
+ copyPlanesToOutput(output, this.channelBuffers, usableFrames);
272
+ if (this.cueOutput) {
273
+ const cue = outputs[1];
274
+ if (cue) {
275
+ copyPlanesToOutput(cue, this.monitorBuffers, usableFrames);
243
276
  }
244
277
  }
245
278
  this.publishClipPageRequests();
@@ -256,6 +289,12 @@ export class SonareRealtimeEngineWorkletProcessor {
256
289
  }
257
290
  }
258
291
 
292
+ private reacquireMonitorBuffers(): void {
293
+ for (let ch = 0; ch < this.channelCount; ch++) {
294
+ this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
295
+ }
296
+ }
297
+
259
298
  receiveCommand(command: SonareEngineCommandRecord): void {
260
299
  if (!this.closed) {
261
300
  this.safeApplyCommand(command);
@@ -348,6 +387,14 @@ export class SonareRealtimeEngineWorkletProcessor {
348
387
  }
349
388
  break;
350
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
+ }
351
398
  case 'syncClipPageCommit': {
352
399
  const providerId = this.pagedClipProviders.get(message.clipId);
353
400
  const clip = message.clip ?? this.pendingPagedClips.get(message.clipId);
@@ -408,11 +455,69 @@ export class SonareRealtimeEngineWorkletProcessor {
408
455
  );
409
456
  }
410
457
  break;
411
- 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();
412
500
  if (message.buses) {
413
501
  this.engine.setTrackBuses(message.buses);
414
502
  }
415
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
+ }
416
521
  for (const strip of message.trackStrips ?? []) {
417
522
  this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
418
523
  }
@@ -425,7 +530,46 @@ export class SonareRealtimeEngineWorkletProcessor {
425
530
  for (const binding of message.laneSidechains ?? []) {
426
531
  this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
427
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
+ }
428
571
  break;
572
+ }
429
573
  case 'syncCapture':
430
574
  this.engine.setCaptureBuffer(message.channels, message.bufferFrames);
431
575
  this.engine.setCaptureSource(message.source);
@@ -438,6 +582,9 @@ export class SonareRealtimeEngineWorkletProcessor {
438
582
  case 'syncMasterStripEqBand':
439
583
  this.engine.setMasterStripEqBandJson(message.bandIndex, message.bandJson);
440
584
  break;
585
+ case 'syncBusStripEqBand':
586
+ this.engine.setBusStripEqBandJson(message.busId, message.bandIndex, message.bandJson);
587
+ break;
441
588
  case 'syncTrackStripInsertBypassed':
442
589
  this.engine.setTrackStripInsertBypassed(
443
590
  message.trackId,
@@ -454,26 +601,65 @@ export class SonareRealtimeEngineWorkletProcessor {
454
601
  );
455
602
  break;
456
603
  case 'syncTrackStripInsertParamByName':
457
- this.engine.setTrackStripInsertParamByName(
458
- message.trackId,
459
- message.insertIndex,
460
- message.paramName,
461
- message.value,
462
- );
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
+ }
463
622
  break;
464
623
  case 'syncMasterStripInsertParamByName':
465
- this.engine.setMasterStripInsertParamByName(
466
- message.insertIndex,
467
- message.paramName,
468
- message.value,
469
- );
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
+ }
470
638
  break;
471
639
  case 'syncBusStripInsertParamByName':
472
- 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(
473
659
  message.busId,
474
660
  message.insertIndex,
475
- message.paramName,
476
- message.value,
661
+ message.bypassed,
662
+ message.resetOnBypass,
477
663
  );
478
664
  break;
479
665
  case 'syncTrackStripPan':
@@ -488,6 +674,18 @@ export class SonareRealtimeEngineWorkletProcessor {
488
674
  case 'syncTrackStripDualPan':
489
675
  this.engine.setTrackStripDualPan(message.trackId, message.leftPan, message.rightPan);
490
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;
491
689
  case 'syncTrackStripChannelDelaySamples':
492
690
  this.engine.setTrackStripChannelDelaySamples(message.trackId, message.delaySamples);
493
691
  break;
@@ -539,8 +737,36 @@ export class SonareRealtimeEngineWorkletProcessor {
539
737
  message.renderFrame,
540
738
  );
541
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;
542
768
  case 'syncMidiUmp':
543
- this.engine.pushMidiUmp(message.destinationId, message.word0, message.renderFrame);
769
+ this.engine.pushMidiUmp(message.destinationId, [message.word0], message.renderFrame);
544
770
  break;
545
771
  case 'syncMidiSysex':
546
772
  this.engine.pushMidiSysex(message.destinationId, message.data, message.renderFrame);
@@ -593,6 +819,31 @@ export class SonareRealtimeEngineWorkletProcessor {
593
819
  message.portTimeSamples,
594
820
  );
595
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;
596
847
  }
597
848
  }
598
849
 
@@ -680,11 +931,11 @@ export class SonareRealtimeEngineWorkletProcessor {
680
931
  }
681
932
  }
682
933
 
683
- private drainCommands(): void {
934
+ private drainCommands(limit = 64): void {
684
935
  if (!this.commandRing) {
685
936
  return;
686
937
  }
687
- for (let i = 0; i < 64; i++) {
938
+ for (let i = 0; i < limit; i++) {
688
939
  const command = popSonareEngineCommandRingBuffer(this.commandRing);
689
940
  if (!command) {
690
941
  return;
@@ -900,14 +1151,15 @@ export class SonareRealtimeEngineWorkletProcessor {
900
1151
  if (meter.frame !== this.lastMeterFrame) {
901
1152
  this.lastMeterFrame = meter.frame;
902
1153
  }
903
- // Prefer the lock-free SAB meter ring (matching the telemetry path and
904
- // SonareWorkletProcessor); only fall back to structured-clone postMessage
905
- // when no ring was provided, so we do not allocate/post from the audio
906
- // render callback in SAB mode.
907
- if (this.meterRing) {
908
- 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);
909
1162
  } else {
910
- this.transport?.onMeter?.(meter);
911
1163
  this.transport?.postMessage?.(meter);
912
1164
  }
913
1165
  }
@@ -926,34 +1178,18 @@ export class SonareRealtimeEngineWorkletProcessor {
926
1178
  Atomics.store(ring.header, 0, writeIndex + 1);
927
1179
  }
928
1180
 
929
- private writeMeterRing(meter: SonareWorkletMeterSnapshot): void {
930
- const ring = this.meterRing;
931
- if (!ring) {
932
- return;
933
- }
934
- const writeIndex = Atomics.load(ring.header, 0);
935
- const offset = (writeIndex % ring.capacity) * SONARE_METER_RING_RECORD_FLOATS;
936
- ring.records[offset] = encodeFrameLo(meter.frame);
937
- ring.records[offset + 1] = encodeFrameHi(meter.frame);
938
- ring.records[offset + 2] = meter.targetId;
939
- ring.records[offset + 3] = meter.peakDbL;
940
- ring.records[offset + 4] = meter.peakDbR;
941
- ring.records[offset + 5] = meter.rmsDbL;
942
- ring.records[offset + 6] = meter.rmsDbR;
943
- ring.records[offset + 7] = meter.correlation;
944
- ring.records[offset + 8] = meter.truePeakDbL;
945
- ring.records[offset + 9] = meter.truePeakDbR;
946
- ring.records[offset + 10] = meter.momentaryLufs;
947
- ring.records[offset + 11] = meter.shortTermLufs;
948
- ring.records[offset + 12] = meter.integratedLufs;
949
- ring.records[offset + 13] = meter.gainReductionDb;
950
- Atomics.store(ring.header, 0, writeIndex + 1);
951
- // writeIndex is a free-running monotonic counter, so an overflow guard here
952
- // would fire on essentially every write past the first `capacity` records
953
- // and store an ever-growing value, not a dropped-record count. Readers
954
- // already detect silent overrun via firstReadable = max(readIndex,
955
- // writeIndex - capacity), so header slot 3 is left at its initial 0.
956
- }
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.
957
1193
 
958
1194
  // Drains the engine's scope producer (FFT spectrum + goniometer points) into
959
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