@libraz/libsonare 1.7.2 → 1.8.1

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 +7 -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 +874 -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 +502 -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 +134 -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 +4173 -1621
  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 +483 -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 +654 -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 +528 -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 +165 -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 +893 -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 +3945 -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 +340 -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 +164 -0
  179. package/dist/worklet/engine-mixer-facade.d.ts.map +1 -0
  180. package/dist/worklet/engine-node.d.ts +83 -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 +72 -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 +75 -0
  193. package/dist/worklet/engine-strips.d.ts.map +1 -0
  194. package/dist/worklet/engine-sync.d.ts +39 -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 +418 -0
  199. package/dist/worklet/engine.d.ts.map +1 -0
  200. package/dist/worklet/guards.d.ts +53 -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 +331 -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 +3200 -541
  215. package/dist/worklet.js.map +1 -1
  216. package/package.json +23 -12
  217. package/src/_effects_common.ts +47 -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 +101 -22
  226. package/src/effects_note_ops.ts +683 -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 +388 -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 +288 -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 +441 -32
  246. package/src/mastering_dynamics.ts +22 -11
  247. package/src/metering.ts +67 -24
  248. package/src/mixer.ts +251 -22
  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 +271 -271
  259. package/src/public_types.ts +122 -3
  260. package/src/public_types_acoustic.ts +112 -3
  261. package/src/public_types_mastering.ts +275 -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 +196 -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 +491 -5
  268. package/src/quick_analysis.ts +203 -26
  269. package/src/realtime_engine.ts +773 -34
  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 +1158 -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 +202 -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 +800 -32
  287. package/src/worklet/engine-node.ts +99 -29
  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 +332 -93
  291. package/src/worklet/engine-register.ts +32 -18
  292. package/src/worklet/engine-strips.ts +275 -9
  293. package/src/worklet/engine-sync.ts +20 -7
  294. package/src/worklet/engine.ts +394 -48
  295. package/src/worklet/guards.ts +195 -44
  296. package/src/worklet/messages.ts +229 -2
  297. package/src/worklet/mixer-processor.ts +117 -48
  298. package/src/worklet/playback-processor.ts +300 -0
  299. package/src/worklet/protocol.ts +82 -11
  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,13 @@
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 {
5
+ requireChannelCount,
6
+ requireIntegerOption,
7
+ resolveScopeBandCount,
8
+ SONARE_SCOPE_MAX_BANDS,
9
+ SONARE_SCOPE_MAX_POINTS,
10
+ } from './guards';
4
11
  import {
5
12
  DEFAULT_METRONOME_CONFIG,
6
13
  type ResolvedMetronomeConfig,
@@ -39,37 +46,13 @@ import {
39
46
  type SonareEngineTelemetryRecord,
40
47
  type SonareEngineTelemetryRingBuffer,
41
48
  SonareEngineTelemetryType,
42
- type SonareWorkletMeterSnapshot,
49
+ type SonareWorkletScopeSnapshot,
43
50
  scopeRingFromSharedBuffer,
44
51
  telemetryFromEngine,
45
52
  writeInt64Words,
46
53
  writeSonareEngineTelemetryRingBuffer,
47
54
  } from './protocol';
48
55
 
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
56
  function captureTransferList(channels: readonly Float32Array[]): Transferable[] {
74
57
  const transfers: ArrayBuffer[] = [];
75
58
  const seen = new Set<ArrayBuffer>();
@@ -107,6 +90,9 @@ export class SonareRealtimeEngineWorkletProcessor {
107
90
  private transport?: WorkletTransport;
108
91
  private meterIntervalFrames: number;
109
92
  private lastMeterFrame = Number.NEGATIVE_INFINITY;
93
+ private scopeIntervalFrames: number;
94
+ private scopeEnabled: boolean;
95
+ private scopeBands: number;
110
96
  // Latest metronome gains/click length pushed via 'syncMetronome'. The
111
97
  // SetMetronome command only toggles enabled state; the config arrives here.
112
98
  private metronomeConfig: ResolvedMetronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
@@ -141,9 +127,15 @@ export class SonareRealtimeEngineWorkletProcessor {
141
127
  ) {
142
128
  this.sampleRate = options.sampleRate ?? 48000;
143
129
  this.blockSize = options.blockSize ?? 128;
144
- this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
130
+ this.channelCount = requireChannelCount(options.channelCount, 2);
145
131
  this.transport = transport;
146
- this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
132
+ // Zero is the off switch for meter publication, so the floor is 0.
133
+ this.meterIntervalFrames = requireIntegerOption(
134
+ options.meterIntervalFrames,
135
+ 2048,
136
+ 'meterIntervalFrames',
137
+ 0,
138
+ );
147
139
  this.commandRing = options.commandSharedBuffer
148
140
  ? this.commandRingFromSharedBuffer(options.commandSharedBuffer, options.commandRingCapacity)
149
141
  : undefined;
@@ -163,6 +155,16 @@ export class SonareRealtimeEngineWorkletProcessor {
163
155
  options.scopeBands,
164
156
  )
165
157
  : undefined;
158
+ this.scopeIntervalFrames = requireIntegerOption(
159
+ options.scopeIntervalFrames,
160
+ this.scopeRing ? this.blockSize : 0,
161
+ 'scopeIntervalFrames',
162
+ 0,
163
+ );
164
+ this.scopeBands = resolveScopeBandCount(options.scopeBands);
165
+ this.scopeEnabled =
166
+ this.scopeIntervalFrames > 0 &&
167
+ (this.scopeRing !== undefined || typeof this.transport?.postMessage === 'function');
166
168
  this.clipPageRequestRing = options.clipPageRequestSharedBuffer
167
169
  ? clipPageRequestRingFromSharedBuffer(
168
170
  options.clipPageRequestSharedBuffer,
@@ -175,10 +177,13 @@ export class SonareRealtimeEngineWorkletProcessor {
175
177
  options.externalMidiRingCapacity,
176
178
  )
177
179
  : undefined;
180
+ if (this.commandRing && this.commandRing.capacity > 65536) {
181
+ throw new RangeError('commandRingCapacity must be at most 65536');
182
+ }
178
183
  this.engine = new RealtimeEngine(
179
184
  this.sampleRate,
180
185
  this.blockSize,
181
- 1024,
186
+ Math.max(1024, this.commandRing?.capacity ?? 0),
182
187
  1024,
183
188
  this.channelCount,
184
189
  );
@@ -197,12 +202,12 @@ export class SonareRealtimeEngineWorkletProcessor {
197
202
  this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
198
203
  }
199
204
  }
200
- // Arm the engine's scope producer only when a scope ring was provided. The
201
- // band count follows the ring's record layout so writeScopeRing never
202
- // overruns its slot.
203
- if (this.scopeRing) {
204
- const interval = Math.max(1, Math.floor(options.scopeIntervalFrames ?? this.blockSize));
205
- this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
205
+ // Arm the native scope producer only when a SAB ring or postMessage sink exists.
206
+ if (this.scopeEnabled) {
207
+ this.engine.configureScopeTelemetry(
208
+ this.scopeIntervalFrames,
209
+ Math.min(this.scopeRing?.bands ?? this.scopeBands, SONARE_SCOPE_MAX_BANDS),
210
+ );
206
211
  }
207
212
  }
208
213
 
@@ -395,6 +400,14 @@ export class SonareRealtimeEngineWorkletProcessor {
395
400
  }
396
401
  break;
397
402
  }
403
+ case 'syncClipPagePrefetchFrames': {
404
+ this.engine.setClipPagePrefetchFrames(message.frames);
405
+ break;
406
+ }
407
+ case 'syncWarpVoiceCapacity': {
408
+ this.engine.setWarpVoiceCapacity(message.voices);
409
+ break;
410
+ }
398
411
  case 'syncClipPageCommit': {
399
412
  const providerId = this.pagedClipProviders.get(message.clipId);
400
413
  const clip = message.clip ?? this.pendingPagedClips.get(message.clipId);
@@ -455,7 +468,47 @@ export class SonareRealtimeEngineWorkletProcessor {
455
468
  );
456
469
  }
457
470
  break;
458
- case 'syncMixer':
471
+ case 'syncMixer': {
472
+ if (message.sidechainDelta === true) {
473
+ // Sidechain setters send a binding delta through this message type.
474
+ // Their `lanes` field is context, not a request to rebuild routing;
475
+ // rebuilding it here would discard live insert smoother slots.
476
+ for (const binding of message.laneSidechains ?? []) {
477
+ this.engine.setLaneSidechain(
478
+ binding.trackId,
479
+ binding.insertIndex,
480
+ binding.sourceTrackId,
481
+ );
482
+ }
483
+ for (const binding of message.busSidechains ?? []) {
484
+ this.engine.setBusSidechain(
485
+ binding.busId,
486
+ binding.insertIndex,
487
+ binding.sourceKind,
488
+ binding.sourceId,
489
+ );
490
+ }
491
+ for (const binding of message.masterSidechains ?? []) {
492
+ this.engine.setMasterSidechain(
493
+ binding.insertIndex,
494
+ binding.sourceKind,
495
+ binding.sourceId,
496
+ );
497
+ }
498
+ break;
499
+ }
500
+ // SAB commands are published before the control thread posts this
501
+ // structural sync. Drain the whole snapshot so a future command past
502
+ // the normal 64-per-render budget cannot enter native after the strip
503
+ // replacement has invalidated its target.
504
+ if (this.commandRing) {
505
+ const write = Atomics.load(this.commandRing.header, 0);
506
+ const read = Atomics.load(this.commandRing.header, 1);
507
+ this.drainCommands(Math.min(this.commandRing.capacity, Math.max(0, write - read)));
508
+ }
509
+ // Complete earlier immediate edits in message order. Scheduled
510
+ // commands remain queued for their render frame.
511
+ this.engine.applyCommandsDueNowPreservingFuture();
459
512
  if (message.buses) {
460
513
  this.engine.setTrackBuses(message.buses);
461
514
  }
@@ -469,10 +522,62 @@ export class SonareRealtimeEngineWorkletProcessor {
469
522
  if (message.masterStripJson) {
470
523
  this.engine.setMasterStripJson(message.masterStripJson);
471
524
  }
525
+ for (const target of message.insertBaseResets ?? []) {
526
+ switch (target.kind) {
527
+ case 'track':
528
+ this.engine.clearTrackInsertParameterBases(target.trackId);
529
+ break;
530
+ case 'bus':
531
+ this.engine.clearBusInsertParameterBases(target.busId);
532
+ break;
533
+ case 'master':
534
+ this.engine.clearMasterInsertParameterBases();
535
+ break;
536
+ }
537
+ }
472
538
  for (const binding of message.laneSidechains ?? []) {
473
539
  this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
474
540
  }
541
+ for (const binding of message.busSidechains ?? []) {
542
+ this.engine.setBusSidechain(
543
+ binding.busId,
544
+ binding.insertIndex,
545
+ binding.sourceKind,
546
+ binding.sourceId,
547
+ );
548
+ }
549
+ for (const binding of message.masterSidechains ?? []) {
550
+ this.engine.setMasterSidechain(binding.insertIndex, binding.sourceKind, binding.sourceId);
551
+ }
552
+ for (const override of message.insertParamOverrides ?? []) {
553
+ switch (override.kind) {
554
+ case 'track':
555
+ this.engine.restoreTrackStripInsertParamByName(
556
+ override.trackId,
557
+ override.insertIndex,
558
+ override.paramName,
559
+ override.value,
560
+ );
561
+ break;
562
+ case 'bus':
563
+ this.engine.restoreBusStripInsertParamByName(
564
+ override.busId,
565
+ override.insertIndex,
566
+ override.paramName,
567
+ override.value,
568
+ );
569
+ break;
570
+ case 'master':
571
+ this.engine.restoreMasterStripInsertParamByName(
572
+ override.insertIndex,
573
+ override.paramName,
574
+ override.value,
575
+ );
576
+ break;
577
+ }
578
+ }
475
579
  break;
580
+ }
476
581
  case 'syncCapture':
477
582
  this.engine.setCaptureBuffer(message.channels, message.bufferFrames);
478
583
  this.engine.setCaptureSource(message.source);
@@ -485,6 +590,9 @@ export class SonareRealtimeEngineWorkletProcessor {
485
590
  case 'syncMasterStripEqBand':
486
591
  this.engine.setMasterStripEqBandJson(message.bandIndex, message.bandJson);
487
592
  break;
593
+ case 'syncBusStripEqBand':
594
+ this.engine.setBusStripEqBandJson(message.busId, message.bandIndex, message.bandJson);
595
+ break;
488
596
  case 'syncTrackStripInsertBypassed':
489
597
  this.engine.setTrackStripInsertBypassed(
490
598
  message.trackId,
@@ -501,26 +609,65 @@ export class SonareRealtimeEngineWorkletProcessor {
501
609
  );
502
610
  break;
503
611
  case 'syncTrackStripInsertParamByName':
504
- this.engine.setTrackStripInsertParamByName(
505
- message.trackId,
506
- message.insertIndex,
507
- message.paramName,
508
- message.value,
509
- );
612
+ this.engine.applyCommandsDueNowPreservingFuture();
613
+ if (
614
+ !this.engine.applyTrackStripInsertParamByNameNow(
615
+ message.trackId,
616
+ message.insertIndex,
617
+ message.paramName,
618
+ message.value,
619
+ )
620
+ ) {
621
+ // A burst can exhaust the fixed smoother slots before the next
622
+ // render quantum. Apply the last knob value exactly in that case.
623
+ this.engine.restoreTrackStripInsertParamByName(
624
+ message.trackId,
625
+ message.insertIndex,
626
+ message.paramName,
627
+ message.value,
628
+ );
629
+ }
510
630
  break;
511
631
  case 'syncMasterStripInsertParamByName':
512
- this.engine.setMasterStripInsertParamByName(
513
- message.insertIndex,
514
- message.paramName,
515
- message.value,
516
- );
632
+ this.engine.applyCommandsDueNowPreservingFuture();
633
+ if (
634
+ !this.engine.applyMasterStripInsertParamByNameNow(
635
+ message.insertIndex,
636
+ message.paramName,
637
+ message.value,
638
+ )
639
+ ) {
640
+ this.engine.restoreMasterStripInsertParamByName(
641
+ message.insertIndex,
642
+ message.paramName,
643
+ message.value,
644
+ );
645
+ }
517
646
  break;
518
647
  case 'syncBusStripInsertParamByName':
519
- this.engine.setBusStripInsertParamByName(
648
+ this.engine.applyCommandsDueNowPreservingFuture();
649
+ if (
650
+ !this.engine.applyBusStripInsertParamByNameNow(
651
+ message.busId,
652
+ message.insertIndex,
653
+ message.paramName,
654
+ message.value,
655
+ )
656
+ ) {
657
+ this.engine.restoreBusStripInsertParamByName(
658
+ message.busId,
659
+ message.insertIndex,
660
+ message.paramName,
661
+ message.value,
662
+ );
663
+ }
664
+ break;
665
+ case 'syncBusStripInsertBypassed':
666
+ this.engine.setBusStripInsertBypassed(
520
667
  message.busId,
521
668
  message.insertIndex,
522
- message.paramName,
523
- message.value,
669
+ message.bypassed,
670
+ message.resetOnBypass,
524
671
  );
525
672
  break;
526
673
  case 'syncTrackStripPan':
@@ -535,6 +682,18 @@ export class SonareRealtimeEngineWorkletProcessor {
535
682
  case 'syncTrackStripDualPan':
536
683
  this.engine.setTrackStripDualPan(message.trackId, message.leftPan, message.rightPan);
537
684
  break;
685
+ case 'syncBusStripPan':
686
+ this.engine.setBusStripPan(message.busId, message.pan);
687
+ break;
688
+ case 'syncBusStripPanLaw':
689
+ this.engine.setBusStripPanLaw(message.busId, message.panLaw);
690
+ break;
691
+ case 'syncBusStripPanMode':
692
+ this.engine.setBusStripPanMode(message.busId, message.panMode);
693
+ break;
694
+ case 'syncBusStripDualPan':
695
+ this.engine.setBusStripDualPan(message.busId, message.leftPan, message.rightPan);
696
+ break;
538
697
  case 'syncTrackStripChannelDelaySamples':
539
698
  this.engine.setTrackStripChannelDelaySamples(message.trackId, message.delaySamples);
540
699
  break;
@@ -586,8 +745,36 @@ export class SonareRealtimeEngineWorkletProcessor {
586
745
  message.renderFrame,
587
746
  );
588
747
  break;
748
+ case 'syncMidiPitchBend':
749
+ this.engine.pushMidiPitchBend(
750
+ message.destinationId,
751
+ message.group,
752
+ message.channel,
753
+ message.data0,
754
+ message.renderFrame,
755
+ );
756
+ break;
757
+ case 'syncMidiChannelPressure':
758
+ this.engine.pushMidiChannelPressure(
759
+ message.destinationId,
760
+ message.group,
761
+ message.channel,
762
+ message.data0,
763
+ message.renderFrame,
764
+ );
765
+ break;
766
+ case 'syncMidiPolyPressure':
767
+ this.engine.pushMidiPolyPressure(
768
+ message.destinationId,
769
+ message.group,
770
+ message.channel,
771
+ message.data0,
772
+ message.data1,
773
+ message.renderFrame,
774
+ );
775
+ break;
589
776
  case 'syncMidiUmp':
590
- this.engine.pushMidiUmp(message.destinationId, message.word0, message.renderFrame);
777
+ this.engine.pushMidiUmp(message.destinationId, [message.word0], message.renderFrame);
591
778
  break;
592
779
  case 'syncMidiSysex':
593
780
  this.engine.pushMidiSysex(message.destinationId, message.data, message.renderFrame);
@@ -640,6 +827,31 @@ export class SonareRealtimeEngineWorkletProcessor {
640
827
  message.portTimeSamples,
641
828
  );
642
829
  break;
830
+ case 'syncMidiInputPitchBend':
831
+ this.engine.pushMidiInputPitchBend(
832
+ message.group,
833
+ message.channel,
834
+ message.data0,
835
+ message.portTimeSamples,
836
+ );
837
+ break;
838
+ case 'syncMidiInputChannelPressure':
839
+ this.engine.pushMidiInputChannelPressure(
840
+ message.group,
841
+ message.channel,
842
+ message.data0,
843
+ message.portTimeSamples,
844
+ );
845
+ break;
846
+ case 'syncMidiInputPolyPressure':
847
+ this.engine.pushMidiInputPolyPressure(
848
+ message.group,
849
+ message.channel,
850
+ message.data0,
851
+ message.data1,
852
+ message.portTimeSamples,
853
+ );
854
+ break;
643
855
  }
644
856
  }
645
857
 
@@ -727,11 +939,11 @@ export class SonareRealtimeEngineWorkletProcessor {
727
939
  }
728
940
  }
729
941
 
730
- private drainCommands(): void {
942
+ private drainCommands(limit = 64): void {
731
943
  if (!this.commandRing) {
732
944
  return;
733
945
  }
734
- for (let i = 0; i < 64; i++) {
946
+ for (let i = 0; i < limit; i++) {
735
947
  const command = popSonareEngineCommandRingBuffer(this.commandRing);
736
948
  if (!command) {
737
949
  return;
@@ -852,6 +1064,9 @@ export class SonareRealtimeEngineWorkletProcessor {
852
1064
  this.engine.setTrackMonitorMode(laneIndex, mode as 0 | 1 | 2, sampleTime);
853
1065
  break;
854
1066
  }
1067
+ case SonareEngineCommandType.ResetMasterLoudnessMeter:
1068
+ this.engine.resetMasterLoudnessMeter(sampleTime);
1069
+ break;
855
1070
  default:
856
1071
  this.publishTelemetryRecord({
857
1072
  type: SonareEngineTelemetryType.Error,
@@ -947,14 +1162,15 @@ export class SonareRealtimeEngineWorkletProcessor {
947
1162
  if (meter.frame !== this.lastMeterFrame) {
948
1163
  this.lastMeterFrame = meter.frame;
949
1164
  }
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);
1165
+ // The ring branch returned above, so this is the structured-clone
1166
+ // fallback. `onMeter` and `postMessage` are alternative channels for the
1167
+ // same record, not a broadcast pair: the AudioWorklet registration
1168
+ // resolves BOTH to `port.postMessage`, so calling them in turn delivered
1169
+ // every record twice and doubled the host's peak-hold decay rate. Take
1170
+ // the first channel the transport offers.
1171
+ if (this.transport?.onMeter) {
1172
+ this.transport.onMeter(meter);
956
1173
  } else {
957
- this.transport?.onMeter?.(meter);
958
1174
  this.transport?.postMessage?.(meter);
959
1175
  }
960
1176
  }
@@ -970,49 +1186,72 @@ export class SonareRealtimeEngineWorkletProcessor {
970
1186
  for (let field = 0; field < 11; field++) {
971
1187
  ring.records[offset + 3 + field] = this.engine.meterScratchValue(field);
972
1188
  }
1189
+ ring.records[offset + 14] = this.engine.meterScratchInputPeakDbL();
1190
+ ring.records[offset + 15] = this.engine.meterScratchInputPeakDbR();
973
1191
  Atomics.store(ring.header, 0, writeIndex + 1);
974
1192
  }
975
1193
 
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
- }
1194
+ // A snapshot-object ring writer used to live here as well. It was
1195
+ // unreachable: publishMeters returns after the writeMeterScratch loop
1196
+ // whenever a ring exists, so the ring is never fed from a materialised
1197
+ // snapshot. writeMeterScratch above is the only ring writer, and it keeps the
1198
+ // render callback allocation-free by reading the engine's scratch registers
1199
+ // directly.
1200
+ //
1201
+ // writeIndex is a free-running monotonic counter, so an overflow guard would
1202
+ // fire on essentially every write past the first `capacity` records and store
1203
+ // an ever-growing value, not a dropped-record count. Readers already detect
1204
+ // silent overrun via firstReadable = max(readIndex, writeIndex - capacity),
1205
+ // so header slot 3 is left at its initial 0.
1004
1206
 
1005
1207
  // Drains the engine's scope producer (FFT spectrum + goniometer points) into
1006
- // the lock-free SAB scope ring. No allocation on the render path: records are
1007
- // written field-by-field into the ring.
1208
+ // the lock-free SAB scope ring or the bounded postMessage fallback. The SAB
1209
+ // path remains allocation-free; postMessage materialises only records that
1210
+ // are ready to publish.
1008
1211
  private publishScope(): void {
1009
- const ring = this.scopeRing;
1010
- if (!ring) {
1212
+ if (!this.scopeEnabled) {
1011
1213
  return;
1012
1214
  }
1215
+ const ring = this.scopeRing;
1216
+ const transport = this.transport;
1013
1217
  for (let count = 0; count < 64 && this.engine.popScopeTelemetryToScratch(); count++) {
1014
- this.writeScopeScratch(ring);
1218
+ if (ring) {
1219
+ this.writeScopeScratch(ring);
1220
+ } else {
1221
+ this.postScopeScratch(transport);
1222
+ }
1223
+ }
1224
+ }
1225
+
1226
+ private postScopeScratch(transport: WorkletTransport | undefined): void {
1227
+ if (!transport?.postMessage) {
1228
+ return;
1229
+ }
1230
+ const bandCount = Math.min(
1231
+ SONARE_SCOPE_MAX_BANDS,
1232
+ Math.max(0, Math.trunc(Number(this.engine.scopeScratchBandCount()))),
1233
+ );
1234
+ const pointCount = Math.min(
1235
+ SONARE_SCOPE_MAX_POINTS,
1236
+ Math.max(0, Math.trunc(Number(this.engine.scopeScratchPointCount()))),
1237
+ );
1238
+ const bands = new Float32Array(bandCount);
1239
+ for (let i = 0; i < bandCount; i++) {
1240
+ bands[i] = this.engine.scopeScratchBand(i);
1241
+ }
1242
+ const points = new Float32Array(pointCount * 2);
1243
+ for (let i = 0; i < pointCount; i++) {
1244
+ points[2 * i] = this.engine.scopeScratchPointLeft(i);
1245
+ points[2 * i + 1] = this.engine.scopeScratchPointRight(i);
1015
1246
  }
1247
+ const scope: SonareWorkletScopeSnapshot = {
1248
+ type: 'scope',
1249
+ targetId: this.engine.scopeScratchTargetId(),
1250
+ frame: Number(this.engine.scopeScratchRenderFrame()),
1251
+ bands,
1252
+ points,
1253
+ };
1254
+ transport.postMessage(scope, [bands.buffer, points.buffer]);
1016
1255
  }
1017
1256
 
1018
1257
  private writeScopeScratch(ring: SharedScopeRingWriter): void {
@@ -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