@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,9 +1,26 @@
1
- import type { EngineBus, EngineTrackLane, EngineTrackSend, RealtimeEngine } from '../index';
1
+ import { sidechainSourceKindCode } from '../codes';
2
+ import type {
3
+ EngineBus,
4
+ EngineTrackLane,
5
+ EngineTrackSend,
6
+ RealtimeEngine,
7
+ SidechainSourceKind,
8
+ } from '../index';
2
9
  import { normalizeTrackLanes } from './engine-offline';
3
- import { buildMixerLanes } from './engine-sync';
4
- import type { SonareEngineSyncMessage } from './messages';
10
+ import { buildMixerLanes, resolveTargetId } from './engine-sync';
11
+ import type { SonareEngineSyncMessage, SonareEngineSyncMixerInsertParamOverride } from './messages';
5
12
  import { ENGINE_MIXER_PARAM_FADER_DB, engineMixerBusTarget } from './protocol';
6
13
 
14
+ /** A latest by-name insert value retained across a later topology replay. */
15
+ export interface InsertParamOverride {
16
+ target: StripJsonTarget;
17
+ insertIndex: number;
18
+ paramName: string;
19
+ value: number;
20
+ }
21
+
22
+ export type InsertParamOverrideMap = Map<string, InsertParamOverride>;
23
+
7
24
  /**
8
25
  * Collaborator surface the mixer/routing setters need from the owning
9
26
  * {@link SonareEngine}: the offline engine they mirror writes into, the mutable
@@ -15,38 +32,507 @@ export interface EngineMixerContext {
15
32
  readonly trackLaneIds: number[];
16
33
  readonly trackSends: Map<number, EngineTrackSend[]>;
17
34
  readonly trackOutputBus: Map<number, number>;
35
+ readonly trackSourceChannelLayout: Map<number, number>;
18
36
  readonly laneSidechains: Map<
19
37
  string,
20
38
  { trackId: number; insertIndex: number; sourceTrackId: number }
21
39
  >;
40
+ readonly busSidechains: Map<
41
+ string,
42
+ { busId: number; insertIndex: number; sourceKind: number; sourceId: number }
43
+ >;
44
+ readonly masterSidechains: Map<
45
+ number,
46
+ { insertIndex: number; sourceKind: number; sourceId: number }
47
+ >;
22
48
  readonly buses: EngineBus[];
23
49
  readonly trackStripJson: Map<number, string>;
24
50
  readonly busStripJson: Map<number, string>;
51
+ readonly insertParamOverrides: InsertParamOverrideMap;
52
+ clearInsertAutomationLanes(target: StripJsonTarget, forgetResolvedIds?: boolean): void;
53
+ flushOfflineMirror(): void;
25
54
  postSync(message: SonareEngineSyncMessage): void;
26
55
  ensureTrackLane(target: string | number): number;
27
56
  ensureBus(busId: number): number;
28
57
  mixerLanes(): EngineTrackLane[];
29
- syncMixer(): void;
58
+ syncMixer(
59
+ insertBaseResets?: StripJsonTarget[],
60
+ oneShotInsertParamOverrides?: InsertParamOverride[],
61
+ ): void;
30
62
  sendSmoothedParam(paramId: number, value: number): boolean;
31
63
  getMasterStripJson(): string | undefined;
64
+ cacheMasterStripJson(sceneJson: string): void;
32
65
  }
33
66
 
34
- /** Builds the engine's track-lane descriptors from the current routing stores. */
35
- export function mixerLanes(ctx: EngineMixerContext): EngineTrackLane[] {
36
- return buildMixerLanes(ctx.trackLaneIds, ctx.trackSends, ctx.trackOutputBus);
67
+ /** Addresses one strip's cached scene JSON, the state a syncMixer re-post replays. */
68
+ export type StripJsonTarget =
69
+ | { kind: 'track'; trackId: number }
70
+ | { kind: 'bus'; busId: number }
71
+ | { kind: 'master' };
72
+
73
+ type SceneObject = Record<string, unknown>;
74
+
75
+ function isSceneObject(value: unknown): value is SceneObject {
76
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
77
+ }
78
+
79
+ function hasOwn(object: SceneObject, key: string): boolean {
80
+ return Object.getOwnPropertyDescriptor(object, key) !== undefined;
81
+ }
82
+
83
+ /** Canonicalizes JSON objects without changing array order or string contents. */
84
+ function canonicalJson(value: unknown): string {
85
+ if (Array.isArray(value)) {
86
+ return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
87
+ }
88
+ if (value !== null && typeof value === 'object') {
89
+ const object = value as SceneObject;
90
+ return `{${Object.keys(object)
91
+ .sort()
92
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`)
93
+ .join(',')}}`;
94
+ }
95
+ return JSON.stringify(value);
96
+ }
97
+
98
+ function aliasedValue(
99
+ object: SceneObject,
100
+ primary: string,
101
+ legacy: string,
102
+ ): { present: boolean; value: unknown } {
103
+ if (hasOwn(object, primary)) {
104
+ return { present: true, value: object[primary] };
105
+ }
106
+ if (hasOwn(object, legacy)) {
107
+ return { present: true, value: object[legacy] };
108
+ }
109
+ return { present: false, value: undefined };
110
+ }
111
+
112
+ function insertSignature(insert: unknown): string {
113
+ const object = insert !== null && typeof insert === 'object' ? (insert as SceneObject) : {};
114
+ const slot = object.slot === undefined ? 'pre' : object.slot;
115
+ const processor = aliasedValue(object, 'processor', 'processor_name');
116
+ const params = aliasedValue(object, 'params', 'params_json');
117
+ const sidechain = aliasedValue(object, 'sidechainKey', 'sidechain_key');
118
+ const processorValue = typeof processor.value === 'string' ? processor.value : '';
119
+ const sidechainValue = typeof sidechain.value === 'string' ? sidechain.value : '';
120
+ let paramsValue: unknown;
121
+ // A missing params bag and an empty legacy blob decode alike; "{}" is distinct.
122
+ if (!params.present) {
123
+ paramsValue = ['absent'];
124
+ } else if (typeof params.value === 'string') {
125
+ // Canonicalize legacy text like object params; invalid text stays byte-exact.
126
+ if (params.value.length === 0) {
127
+ paramsValue = ['absent'];
128
+ } else {
129
+ try {
130
+ paramsValue = ['params', canonicalJson(JSON.parse(params.value))];
131
+ } catch {
132
+ paramsValue = ['params-invalid', params.value];
133
+ }
134
+ }
135
+ } else {
136
+ paramsValue = ['params', canonicalJson(params.value)];
137
+ }
138
+ return JSON.stringify([slot, processorValue, paramsValue, sidechainValue]);
37
139
  }
38
140
 
39
141
  /**
40
- * Mirrors the current mixer routing into the offline engine and posts the full
41
- * mixer-sync message (lanes, buses, strip JSON, sidechains) to the worklet.
142
+ * Returns the insert identity used to decide whether native state can stay
143
+ * alive across a strip-scene update. Scalar strip fields are intentionally
144
+ * excluded. Track and master inserts use the native pre-then-post indexing;
145
+ * buses retain their scene order.
42
146
  */
43
- export function syncMixer(ctx: EngineMixerContext): void {
44
- const lanes = mixerLanes(ctx);
147
+ export function decodedInsertSignature(sceneJson: string, target: StripJsonTarget): string {
148
+ const scene = JSON.parse(sceneJson) as {
149
+ strips?: unknown[];
150
+ buses?: unknown[];
151
+ };
152
+ const entries = target.kind === 'bus' ? scene.buses : scene.strips;
153
+ const strip = Array.isArray(entries) ? entries.find(isSceneObject) : undefined;
154
+ const object = isSceneObject(strip) ? strip : {};
155
+ const inserts = Array.isArray(object.inserts) ? object.inserts.filter(isSceneObject) : [];
156
+ const ordered =
157
+ target.kind === 'bus'
158
+ ? inserts
159
+ : [
160
+ ...inserts.filter((insert) => insert.slot !== 'post'),
161
+ ...inserts.filter((insert) => insert.slot === 'post'),
162
+ ];
163
+ return JSON.stringify(ordered.map(insertSignature));
164
+ }
165
+
166
+ function constructionValue(
167
+ engine: RealtimeEngine,
168
+ target: StripJsonTarget,
169
+ override: InsertParamOverride,
170
+ ): number | undefined {
171
+ let parameterId = -1;
172
+ switch (target.kind) {
173
+ case 'track':
174
+ parameterId = engine.resolveTrackInsertAutomationId(
175
+ target.trackId,
176
+ override.insertIndex,
177
+ override.paramName,
178
+ );
179
+ break;
180
+ case 'bus':
181
+ parameterId = engine.resolveBusInsertAutomationId(
182
+ target.busId,
183
+ override.insertIndex,
184
+ override.paramName,
185
+ );
186
+ break;
187
+ case 'master':
188
+ parameterId = engine.resolveMasterInsertAutomationId(
189
+ override.insertIndex,
190
+ override.paramName,
191
+ );
192
+ break;
193
+ }
194
+ if (parameterId < 0) {
195
+ return undefined;
196
+ }
197
+ return engine.insertParameterConstructedValue(parameterId);
198
+ }
199
+
200
+ /** Builds one-shot restores for manual insert edits before their bookkeeping is forgotten. */
201
+ export function constructionInsertOverrides(
202
+ engine: RealtimeEngine,
203
+ target: StripJsonTarget,
204
+ overrides: InsertParamOverrideMap,
205
+ ): InsertParamOverride[] {
206
+ const restored: InsertParamOverride[] = [];
207
+ for (const override of overrides.values()) {
208
+ if (!sameStripTarget(override.target, target)) {
209
+ continue;
210
+ }
211
+ const value = constructionValue(engine, target, override);
212
+ if (value !== undefined) {
213
+ restored.push({ ...override, value });
214
+ }
215
+ }
216
+ return restored;
217
+ }
218
+
219
+ function applyInsertParamOverride(engine: RealtimeEngine, override: InsertParamOverride): void {
220
+ switch (override.target.kind) {
221
+ case 'track':
222
+ engine.restoreTrackStripInsertParamByName(
223
+ override.target.trackId,
224
+ override.insertIndex,
225
+ override.paramName,
226
+ override.value,
227
+ );
228
+ break;
229
+ case 'bus':
230
+ engine.restoreBusStripInsertParamByName(
231
+ override.target.busId,
232
+ override.insertIndex,
233
+ override.paramName,
234
+ override.value,
235
+ );
236
+ break;
237
+ case 'master':
238
+ engine.restoreMasterStripInsertParamByName(
239
+ override.insertIndex,
240
+ override.paramName,
241
+ override.value,
242
+ );
243
+ break;
244
+ }
245
+ }
246
+
247
+ /** Applies construction restores to the control-side mirror before posting them. */
248
+ export function applyConstructionInsertOverrides(
249
+ engine: RealtimeEngine,
250
+ overrides: readonly InsertParamOverride[],
251
+ ): void {
252
+ for (const override of overrides) {
253
+ applyInsertParamOverride(engine, override);
254
+ }
255
+ }
256
+
257
+ /** Clears the native manual parameter bases for one strip after a replacement. */
258
+ export function clearInsertParameterBases(engine: RealtimeEngine, target: StripJsonTarget): void {
259
+ switch (target.kind) {
260
+ case 'track':
261
+ engine.clearTrackInsertParameterBases(target.trackId);
262
+ break;
263
+ case 'bus':
264
+ engine.clearBusInsertParameterBases(target.busId);
265
+ break;
266
+ case 'master':
267
+ engine.clearMasterInsertParameterBases();
268
+ break;
269
+ }
270
+ }
271
+
272
+ interface MixerRoutingDraft {
273
+ trackLaneIds: number[];
274
+ trackSends: Map<number, EngineTrackSend[]>;
275
+ trackOutputBus: Map<number, number>;
276
+ trackSourceChannelLayout: Map<number, number>;
277
+ }
278
+
279
+ /** An empty construction scene used only within an explicit strip replacement. */
280
+ export function emptyStripJson(target: StripJsonTarget): string {
281
+ switch (target.kind) {
282
+ case 'track':
283
+ return `{"version":1,"strips":[{"id":"track-${target.trackId}"}],"buses":[],"connections":[]}`;
284
+ case 'bus':
285
+ return `{"version":1,"strips":[],"buses":[{"id":"bus-${target.busId}"}],"connections":[]}`;
286
+ case 'master':
287
+ return '{"version":1,"strips":[{"id":"master"}],"buses":[],"connections":[]}';
288
+ }
289
+ }
290
+
291
+ /** Drop bindings whose insert was removed by an explicit scene replacement. */
292
+ export function pruneStripSidechains(
293
+ ctx: EngineMixerContext,
294
+ target: StripJsonTarget,
295
+ sceneJson: string,
296
+ ): void {
297
+ const scene = JSON.parse(sceneJson) as {
298
+ strips?: Array<{ inserts?: unknown[] }>;
299
+ buses?: Array<{ inserts?: unknown[] }>;
300
+ };
301
+ const entry = target.kind === 'bus' ? scene.buses?.[0] : scene.strips?.[0];
302
+ const insertCount = Array.isArray(entry?.inserts) ? entry.inserts.length : 0;
303
+ if (target.kind === 'track') {
304
+ for (const [key, binding] of ctx.laneSidechains) {
305
+ if (binding.trackId === target.trackId && binding.insertIndex >= insertCount) {
306
+ ctx.laneSidechains.delete(key);
307
+ }
308
+ }
309
+ } else if (target.kind === 'bus') {
310
+ for (const [key, binding] of ctx.busSidechains) {
311
+ if (binding.busId === target.busId && binding.insertIndex >= insertCount) {
312
+ ctx.busSidechains.delete(key);
313
+ }
314
+ }
315
+ } else {
316
+ for (const [key, binding] of ctx.masterSidechains) {
317
+ if (binding.insertIndex >= insertCount) {
318
+ ctx.masterSidechains.delete(key);
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ export function hasInsertParamOverrides(
325
+ overrides: InsertParamOverrideMap,
326
+ target: StripJsonTarget,
327
+ ): boolean {
328
+ for (const override of overrides.values()) {
329
+ if (sameStripTarget(override.target, target)) {
330
+ return true;
331
+ }
332
+ }
333
+ return false;
334
+ }
335
+
336
+ /** Apply and validate one candidate scene without rebuilding it through an empty scene. */
337
+ export function applyFullStripJson(
338
+ engine: RealtimeEngine,
339
+ target: StripJsonTarget,
340
+ sceneJson: string,
341
+ ): void {
342
+ const apply = (json: string): void => {
343
+ switch (target.kind) {
344
+ case 'track':
345
+ engine.setTrackStripJson(target.trackId, json);
346
+ break;
347
+ case 'bus':
348
+ engine.setBusStripJson(target.busId, json);
349
+ break;
350
+ case 'master':
351
+ engine.setMasterStripJson(json);
352
+ break;
353
+ }
354
+ };
355
+ engine.applyCommandsDueNowPreservingFuture();
356
+ apply(sceneJson);
357
+ }
358
+
359
+ /** Builds a collision-safe key for one target/insert/parameter tuple. */
360
+ export function insertParamOverrideKey(override: InsertParamOverride): string {
361
+ const targetId =
362
+ override.target.kind === 'track'
363
+ ? override.target.trackId
364
+ : override.target.kind === 'bus'
365
+ ? override.target.busId
366
+ : 'master';
367
+ return JSON.stringify([override.target.kind, targetId, override.insertIndex, override.paramName]);
368
+ }
369
+
370
+ /** Removes all retained insert values for one strip after a full scene replacement. */
371
+ export function clearInsertParamOverrides(
372
+ overrides: InsertParamOverrideMap,
373
+ target: StripJsonTarget,
374
+ ): void {
375
+ for (const [key, override] of overrides) {
376
+ if (sameStripTarget(override.target, target)) {
377
+ overrides.delete(key);
378
+ }
379
+ }
380
+ }
381
+
382
+ function sameStripTarget(left: StripJsonTarget, right: StripJsonTarget): boolean {
383
+ if (left.kind !== right.kind) {
384
+ return false;
385
+ }
386
+ if (left.kind === 'track' && right.kind === 'track') {
387
+ return left.trackId === right.trackId;
388
+ }
389
+ if (left.kind === 'bus' && right.kind === 'bus') {
390
+ return left.busId === right.busId;
391
+ }
392
+ return left.kind === 'master' && right.kind === 'master';
393
+ }
394
+
395
+ function flattenInsertParamOverride(
396
+ override: InsertParamOverride,
397
+ ): SonareEngineSyncMixerInsertParamOverride {
398
+ switch (override.target.kind) {
399
+ case 'track':
400
+ return {
401
+ kind: 'track',
402
+ trackId: override.target.trackId,
403
+ insertIndex: override.insertIndex,
404
+ paramName: override.paramName,
405
+ value: override.value,
406
+ };
407
+ case 'bus':
408
+ return {
409
+ kind: 'bus',
410
+ busId: override.target.busId,
411
+ insertIndex: override.insertIndex,
412
+ paramName: override.paramName,
413
+ value: override.value,
414
+ };
415
+ case 'master':
416
+ return {
417
+ kind: 'master',
418
+ insertIndex: override.insertIndex,
419
+ paramName: override.paramName,
420
+ value: override.value,
421
+ };
422
+ }
423
+ }
424
+
425
+ /** Reads a strip's cached scene JSON; undefined when none was ever set. */
426
+ export function cachedStripJson(
427
+ ctx: EngineMixerContext,
428
+ target: StripJsonTarget,
429
+ ): string | undefined {
430
+ switch (target.kind) {
431
+ case 'track':
432
+ return ctx.trackStripJson.get(target.trackId);
433
+ case 'bus':
434
+ return ctx.busStripJson.get(target.busId);
435
+ case 'master':
436
+ return ctx.getMasterStripJson();
437
+ }
438
+ }
439
+
440
+ /** Replaces a strip's cached scene JSON without syncing it. */
441
+ export function cacheStripJson(
442
+ ctx: EngineMixerContext,
443
+ target: StripJsonTarget,
444
+ sceneJson: string,
445
+ ): void {
446
+ switch (target.kind) {
447
+ case 'track':
448
+ ctx.trackStripJson.set(target.trackId, sceneJson);
449
+ return;
450
+ case 'bus':
451
+ ctx.busStripJson.set(target.busId, sceneJson);
452
+ return;
453
+ case 'master':
454
+ ctx.cacheMasterStripJson(sceneJson);
455
+ return;
456
+ }
457
+ }
458
+
459
+ /** Builds the engine's track-lane descriptors from the current routing stores. */
460
+ export function mixerLanes(ctx: EngineMixerContext): EngineTrackLane[] {
461
+ return buildMixerLanes(
462
+ ctx.trackLaneIds,
463
+ ctx.trackSends,
464
+ ctx.trackOutputBus,
465
+ ctx.trackSourceChannelLayout,
466
+ );
467
+ }
468
+
469
+ function cloneMixerRouting(ctx: EngineMixerContext): MixerRoutingDraft {
470
+ return {
471
+ trackLaneIds: [...ctx.trackLaneIds],
472
+ trackSends: new Map(
473
+ Array.from(ctx.trackSends, ([trackId, sends]) => [
474
+ trackId,
475
+ sends.map((send) => ({ ...send })),
476
+ ]),
477
+ ),
478
+ trackOutputBus: new Map(ctx.trackOutputBus),
479
+ trackSourceChannelLayout: new Map(ctx.trackSourceChannelLayout),
480
+ };
481
+ }
482
+
483
+ function commitMixerRouting(ctx: EngineMixerContext, draft: MixerRoutingDraft): void {
484
+ ctx.trackLaneIds.splice(0, ctx.trackLaneIds.length, ...draft.trackLaneIds);
485
+ ctx.trackSends.clear();
486
+ for (const [trackId, sends] of draft.trackSends) {
487
+ ctx.trackSends.set(
488
+ trackId,
489
+ sends.map((send) => ({ ...send })),
490
+ );
491
+ }
492
+ ctx.trackOutputBus.clear();
493
+ for (const [trackId, busId] of draft.trackOutputBus) {
494
+ ctx.trackOutputBus.set(trackId, busId);
495
+ }
496
+ ctx.trackSourceChannelLayout.clear();
497
+ for (const [trackId, layout] of draft.trackSourceChannelLayout) {
498
+ ctx.trackSourceChannelLayout.set(trackId, layout);
499
+ }
500
+ }
501
+
502
+ function buildDraftMixerLanes(draft: MixerRoutingDraft): EngineTrackLane[] {
503
+ return buildMixerLanes(
504
+ draft.trackLaneIds,
505
+ draft.trackSends,
506
+ draft.trackOutputBus,
507
+ draft.trackSourceChannelLayout,
508
+ );
509
+ }
510
+
511
+ function applyMixerRouting(
512
+ ctx: EngineMixerContext,
513
+ draft: MixerRoutingDraft,
514
+ busesAlreadyApplied: boolean,
515
+ ): { lanes: EngineTrackLane[]; buses: EngineBus[] } {
516
+ const lanes = buildDraftMixerLanes(draft);
45
517
  const buses = ctx.buses.map((bus) => ({ ...bus }));
46
- ctx.offlineEngine.setTrackBuses(buses);
518
+ if (!busesAlreadyApplied) {
519
+ ctx.offlineEngine.setTrackBuses(buses);
520
+ }
47
521
  if (lanes.length > 0) {
522
+ // Not settled: that would snap unrelated insert ramps on a scalar edit.
48
523
  ctx.offlineEngine.setTrackLanes(lanes);
49
524
  }
525
+ return { lanes, buses };
526
+ }
527
+
528
+ function postMixerSync(
529
+ ctx: EngineMixerContext,
530
+ lanes: EngineTrackLane[],
531
+ buses: EngineBus[],
532
+ insertBaseResets: StripJsonTarget[],
533
+ oneShotInsertParamOverrides: InsertParamOverride[],
534
+ ): void {
535
+ replayInsertParamOverrides(ctx, lanes, buses);
50
536
  const trackStrips = Array.from(ctx.trackStripJson, ([trackId, sceneJson]) => ({
51
537
  trackId,
52
538
  sceneJson,
@@ -55,6 +541,10 @@ export function syncMixer(ctx: EngineMixerContext): void {
55
541
  busId,
56
542
  sceneJson,
57
543
  }));
544
+ const insertParamOverrides = [
545
+ ...Array.from(ctx.insertParamOverrides.values(), flattenInsertParamOverride),
546
+ ...oneShotInsertParamOverrides.map(flattenInsertParamOverride),
547
+ ];
58
548
  ctx.postSync({
59
549
  type: 'syncMixer',
60
550
  lanes,
@@ -63,9 +553,94 @@ export function syncMixer(ctx: EngineMixerContext): void {
63
553
  laneSidechains: Array.from(ctx.laneSidechains.values()),
64
554
  busStrips,
65
555
  masterStripJson: ctx.getMasterStripJson(),
556
+ busSidechains: Array.from(ctx.busSidechains.values()),
557
+ masterSidechains: Array.from(ctx.masterSidechains.values()),
558
+ ...(insertParamOverrides.length > 0 ? { insertParamOverrides } : {}),
559
+ ...(insertBaseResets.length > 0 ? { insertBaseResets } : {}),
66
560
  });
67
561
  }
68
562
 
563
+ function syncMixerDraft(
564
+ ctx: EngineMixerContext,
565
+ draft: MixerRoutingDraft,
566
+ busesAlreadyApplied = false,
567
+ insertBaseResets: StripJsonTarget[] = [],
568
+ oneShotInsertParamOverrides: InsertParamOverride[] = [],
569
+ ): void {
570
+ const { lanes, buses } = applyMixerRouting(ctx, draft, busesAlreadyApplied);
571
+ commitMixerRouting(ctx, draft);
572
+ postMixerSync(ctx, lanes, buses, insertBaseResets, oneShotInsertParamOverrides);
573
+ }
574
+
575
+ /**
576
+ * Mirrors the current mixer routing into the offline engine and posts the full
577
+ * mixer-sync message (lanes, buses, strip JSON, sidechains) to the worklet.
578
+ */
579
+ export function syncMixer(
580
+ ctx: EngineMixerContext,
581
+ busesAlreadyApplied = false,
582
+ insertBaseResets: StripJsonTarget[] = [],
583
+ oneShotInsertParamOverrides: InsertParamOverride[] = [],
584
+ ): void {
585
+ syncMixerDraft(
586
+ ctx,
587
+ cloneMixerRouting(ctx),
588
+ busesAlreadyApplied,
589
+ insertBaseResets,
590
+ oneShotInsertParamOverrides,
591
+ );
592
+ }
593
+
594
+ /**
595
+ * Reapplies retained by-name values after native topology setters clear their
596
+ * smoother slots. This runs once per mixer sync, never once per knob tick.
597
+ */
598
+ export function replayInsertParamOverrides(
599
+ ctx: EngineMixerContext,
600
+ lanes = mixerLanes(ctx),
601
+ buses = ctx.buses,
602
+ ): void {
603
+ const activeTrackIds = new Set(lanes.map((lane) => lane.trackId));
604
+ const activeBusIds = new Set(buses.map((bus) => bus.busId));
605
+ for (const [key, override] of ctx.insertParamOverrides) {
606
+ const active =
607
+ override.target.kind === 'track'
608
+ ? activeTrackIds.has(override.target.trackId)
609
+ : override.target.kind === 'bus'
610
+ ? activeBusIds.has(override.target.busId)
611
+ : true;
612
+ if (!active) {
613
+ ctx.insertParamOverrides.delete(key);
614
+ continue;
615
+ }
616
+ switch (override.target.kind) {
617
+ case 'track':
618
+ ctx.offlineEngine.restoreTrackStripInsertParamByName(
619
+ override.target.trackId,
620
+ override.insertIndex,
621
+ override.paramName,
622
+ override.value,
623
+ );
624
+ break;
625
+ case 'bus':
626
+ ctx.offlineEngine.restoreBusStripInsertParamByName(
627
+ override.target.busId,
628
+ override.insertIndex,
629
+ override.paramName,
630
+ override.value,
631
+ );
632
+ break;
633
+ case 'master':
634
+ ctx.offlineEngine.restoreMasterStripInsertParamByName(
635
+ override.insertIndex,
636
+ override.paramName,
637
+ override.value,
638
+ );
639
+ break;
640
+ }
641
+ }
642
+ }
643
+
69
644
  /**
70
645
  * Declares the mixer track lanes in an explicit order.
71
646
  *
@@ -74,6 +649,7 @@ export function syncMixer(ctx: EngineMixerContext): void {
74
649
  * with the already-declared lane ids in their current order and may only
75
650
  * append new track ids after them. Entries carrying `sends` replace that
76
651
  * track's send list; entries without `sends` leave existing sends untouched.
652
+ * The same omission rule preserves an existing source channel layout.
77
653
  *
78
654
  * @param lanes Track ids or lane descriptors in the desired lane order.
79
655
  */
@@ -81,24 +657,51 @@ export function setTrackLanes(
81
657
  ctx: EngineMixerContext,
82
658
  lanes: ReadonlyArray<number | EngineTrackLane>,
83
659
  ): void {
84
- const { entries, ids } = normalizeTrackLanes(ctx.trackLaneIds, lanes);
660
+ const draft = cloneMixerRouting(ctx);
661
+ const { entries, ids } = normalizeTrackLanes(draft.trackLaneIds, lanes);
85
662
  for (const entry of entries) {
86
663
  if (entry.sends) {
87
- ctx.trackSends.set(
664
+ draft.trackSends.set(
88
665
  entry.trackId,
89
666
  entry.sends.map((send) => ({ ...send })),
90
667
  );
91
668
  }
92
669
  if (entry.outputBusId !== undefined) {
93
670
  if (entry.outputBusId === 0) {
94
- ctx.trackOutputBus.delete(entry.trackId);
671
+ draft.trackOutputBus.delete(entry.trackId);
95
672
  } else {
96
- ctx.trackOutputBus.set(entry.trackId, entry.outputBusId);
673
+ draft.trackOutputBus.set(entry.trackId, entry.outputBusId);
97
674
  }
98
675
  }
676
+ if (entry.sourceChannelLayout !== undefined) {
677
+ draft.trackSourceChannelLayout.set(entry.trackId, entry.sourceChannelLayout);
678
+ }
679
+ }
680
+ draft.trackLaneIds = ids;
681
+ syncMixerDraft(ctx, draft, true);
682
+ }
683
+
684
+ function ensureTrackLaneInDraft(draft: MixerRoutingDraft, target: string | number): number {
685
+ const trackId = resolveTargetId(target);
686
+ if (!Number.isInteger(trackId) || trackId <= 0) {
687
+ throw new RangeError(`Invalid track id for mixer lane: ${String(target)}`);
99
688
  }
100
- ctx.trackLaneIds.splice(0, ctx.trackLaneIds.length, ...ids);
101
- ctx.syncMixer();
689
+ const existing = draft.trackLaneIds.indexOf(trackId);
690
+ if (existing >= 0) {
691
+ return existing;
692
+ }
693
+ draft.trackLaneIds.push(trackId);
694
+ return draft.trackLaneIds.length - 1;
695
+ }
696
+
697
+ /** Ensures a lane exists, committing it only after native routing accepts it. */
698
+ export function ensureTrackLane(ctx: EngineMixerContext, target: string | number): number {
699
+ const draft = cloneMixerRouting(ctx);
700
+ const laneIndex = ensureTrackLaneInDraft(draft, target);
701
+ if (draft.trackLaneIds.length !== ctx.trackLaneIds.length) {
702
+ syncMixerDraft(ctx, draft, true);
703
+ }
704
+ return laneIndex;
102
705
  }
103
706
 
104
707
  /**
@@ -110,14 +713,15 @@ export function setTrackOutputBus(
110
713
  target: string | number,
111
714
  busId: number,
112
715
  ): void {
113
- const laneIndex = ctx.ensureTrackLane(target);
114
- const trackId = ctx.trackLaneIds[laneIndex];
716
+ const draft = cloneMixerRouting(ctx);
717
+ const laneIndex = ensureTrackLaneInDraft(draft, target);
718
+ const trackId = draft.trackLaneIds[laneIndex];
115
719
  if (busId === 0) {
116
- ctx.trackOutputBus.delete(trackId);
720
+ draft.trackOutputBus.delete(trackId);
117
721
  } else {
118
- ctx.trackOutputBus.set(trackId, busId);
722
+ draft.trackOutputBus.set(trackId, busId);
119
723
  }
120
- ctx.syncMixer();
724
+ syncMixerDraft(ctx, draft, true);
121
725
  }
122
726
 
123
727
  /**
@@ -139,48 +743,212 @@ export function setLaneSidechain(
139
743
  const sourceIndex = ctx.ensureTrackLane(sourceTarget);
140
744
  sourceTrackId = ctx.trackLaneIds[sourceIndex];
141
745
  }
746
+ ctx.offlineEngine.setLaneSidechain(trackId, insertIndex, sourceTrackId);
142
747
  if (sourceTrackId === 0) {
143
748
  ctx.laneSidechains.delete(key);
144
749
  } else {
145
750
  ctx.laneSidechains.set(key, { trackId, insertIndex, sourceTrackId });
146
751
  }
147
- ctx.offlineEngine.setLaneSidechain(trackId, insertIndex, sourceTrackId);
148
752
  ctx.postSync({
149
753
  type: 'syncMixer',
754
+ sidechainDelta: true,
150
755
  lanes: ctx.mixerLanes(),
151
756
  laneSidechains: [{ trackId, insertIndex, sourceTrackId }],
152
757
  });
153
758
  }
154
759
 
760
+ /**
761
+ * Keys one insert of a bus strip from a track lane or another bus
762
+ * (ducking/sidechainRouter inserts). `sourceId` 0 removes the binding. The
763
+ * binding is cached for resync only once the engine has accepted it.
764
+ */
765
+ export function setBusSidechain(
766
+ ctx: EngineMixerContext,
767
+ busId: number,
768
+ insertIndex: number,
769
+ kind: SidechainSourceKind | number,
770
+ sourceId: number,
771
+ ): void {
772
+ const sourceKind = sidechainSourceKindCode(kind);
773
+ ctx.ensureBus(busId);
774
+ ensureSidechainSource(ctx, sourceKind, sourceId);
775
+ ctx.offlineEngine.setBusSidechain(busId, insertIndex, sourceKind, sourceId);
776
+ const key = `${busId}:${insertIndex}`;
777
+ if (sourceId === 0) {
778
+ ctx.busSidechains.delete(key);
779
+ } else {
780
+ ctx.busSidechains.set(key, { busId, insertIndex, sourceKind, sourceId });
781
+ }
782
+ ctx.postSync({
783
+ type: 'syncMixer',
784
+ sidechainDelta: true,
785
+ lanes: ctx.mixerLanes(),
786
+ busSidechains: [{ busId, insertIndex, sourceKind, sourceId }],
787
+ });
788
+ }
789
+
790
+ /**
791
+ * Keys one insert of the master strip from a track lane or a bus. Same source
792
+ * rules as {@link setBusSidechain}.
793
+ */
794
+ export function setMasterSidechain(
795
+ ctx: EngineMixerContext,
796
+ insertIndex: number,
797
+ kind: SidechainSourceKind | number,
798
+ sourceId: number,
799
+ ): void {
800
+ const sourceKind = sidechainSourceKindCode(kind);
801
+ ensureSidechainSource(ctx, sourceKind, sourceId);
802
+ ctx.offlineEngine.setMasterSidechain(insertIndex, sourceKind, sourceId);
803
+ if (sourceId === 0) {
804
+ ctx.masterSidechains.delete(insertIndex);
805
+ } else {
806
+ ctx.masterSidechains.set(insertIndex, { insertIndex, sourceKind, sourceId });
807
+ }
808
+ ctx.postSync({
809
+ type: 'syncMixer',
810
+ sidechainDelta: true,
811
+ lanes: ctx.mixerLanes(),
812
+ masterSidechains: [{ insertIndex, sourceKind, sourceId }],
813
+ });
814
+ }
815
+
816
+ function ensureSidechainSource(
817
+ ctx: EngineMixerContext,
818
+ sourceKind: number,
819
+ sourceId: number,
820
+ ): void {
821
+ if (sourceId === 0) {
822
+ return;
823
+ }
824
+ if (sourceKind === 1) {
825
+ ctx.ensureBus(sourceId);
826
+ } else {
827
+ ctx.ensureTrackLane(sourceId);
828
+ }
829
+ }
830
+
155
831
  export function setSends(
156
832
  ctx: EngineMixerContext,
157
833
  target: string | number,
158
834
  sends: EngineTrackSend[],
159
835
  ): void {
160
- const laneIndex = ctx.ensureTrackLane(target);
161
- const trackId = ctx.trackLaneIds[laneIndex];
162
- ctx.trackSends.set(
836
+ const draft = cloneMixerRouting(ctx);
837
+ const laneIndex = ensureTrackLaneInDraft(draft, target);
838
+ const trackId = draft.trackLaneIds[laneIndex];
839
+ draft.trackSends.set(
163
840
  trackId,
164
841
  sends.map((send) => ({ ...send })),
165
842
  );
166
- ctx.syncMixer();
843
+ syncMixerDraft(ctx, draft, true);
167
844
  }
168
845
 
169
846
  export function setTrackBuses(ctx: EngineMixerContext, buses: EngineBus[]): void {
847
+ // Validate and apply before pruning retained scenes. Native rejects a bus
848
+ // removal while a lane still routes to it; that failure must leave JS state
849
+ // untouched so a later valid edit can still replay the existing inserts.
850
+ ctx.offlineEngine.setTrackBuses(buses);
851
+ const retainedBusIds = new Set(buses.map((bus) => bus.busId));
852
+ for (const bus of ctx.buses) {
853
+ if (!retainedBusIds.has(bus.busId)) {
854
+ ctx.clearInsertAutomationLanes({ kind: 'bus', busId: bus.busId }, true);
855
+ }
856
+ }
170
857
  ctx.buses.splice(0, ctx.buses.length, ...buses.map((bus) => ({ ...bus })));
171
- ctx.syncMixer();
858
+ const activeBusIds = new Set(ctx.buses.map((bus) => bus.busId));
859
+ for (const busId of ctx.busStripJson.keys()) {
860
+ if (!activeBusIds.has(busId)) {
861
+ ctx.busStripJson.delete(busId);
862
+ }
863
+ }
864
+ for (const [key, binding] of ctx.busSidechains) {
865
+ if (
866
+ !activeBusIds.has(binding.busId) ||
867
+ (binding.sourceKind === 1 && !activeBusIds.has(binding.sourceId))
868
+ ) {
869
+ ctx.busSidechains.delete(key);
870
+ }
871
+ }
872
+ for (const [key, binding] of ctx.masterSidechains) {
873
+ if (binding.sourceKind === 1 && !activeBusIds.has(binding.sourceId)) {
874
+ ctx.masterSidechains.delete(key);
875
+ }
876
+ }
877
+ syncMixer(ctx, true);
172
878
  }
173
879
 
174
880
  export function setBusGain(ctx: EngineMixerContext, busId: number, db: number): boolean {
175
881
  const busIndex = ctx.ensureBus(busId);
176
882
  ctx.buses[busIndex] = { ...ctx.buses[busIndex], busId, gainDb: db };
177
- ctx.offlineEngine.setTrackBuses(ctx.buses);
883
+ // The reserved smoothed parameter updates both engines. Rebuilding the bus
884
+ // topology for every fader tick would reset insert slots and their tails.
178
885
  return ctx.sendSmoothedParam(engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB), db);
179
886
  }
180
887
 
888
+ /** Control-side state a strip-scene replacement reads and updates. */
889
+ export interface StripSceneContext {
890
+ readonly offlineEngine: RealtimeEngine;
891
+ readonly insertParamOverrides: InsertParamOverrideMap;
892
+ clearInsertAutomationLanes(target: StripJsonTarget): void;
893
+ }
894
+
895
+ /**
896
+ * Applies a strip scene to the offline mirror and returns what the realtime
897
+ * sync must repeat: the targets whose manual insert bases it clears and the
898
+ * construction values it restores. A changed insert chain forgets manual edits;
899
+ * an equal chain with manual edits restores each edited parameter's
900
+ * construction value. Native state of equal inserts is kept either way.
901
+ */
902
+ export function replaceStripScene(
903
+ ctx: StripSceneContext,
904
+ target: StripJsonTarget,
905
+ previousJson: string | undefined,
906
+ sceneJson: string,
907
+ ): { insertBaseResets: StripJsonTarget[]; constructionOverrides: InsertParamOverride[] } {
908
+ const chainChanged =
909
+ previousJson !== undefined &&
910
+ decodedInsertSignature(previousJson, target) !== decodedInsertSignature(sceneJson, target);
911
+ const hasOverrides = hasInsertParamOverrides(ctx.insertParamOverrides, target);
912
+ applyFullStripJson(ctx.offlineEngine, target, sceneJson);
913
+ if (!chainChanged && !hasOverrides) {
914
+ return { insertBaseResets: [], constructionOverrides: [] };
915
+ }
916
+ const constructionOverrides = chainChanged
917
+ ? []
918
+ : constructionInsertOverrides(ctx.offlineEngine, target, ctx.insertParamOverrides);
919
+ ctx.clearInsertAutomationLanes(target);
920
+ clearInsertParameterBases(ctx.offlineEngine, target);
921
+ applyConstructionInsertOverrides(ctx.offlineEngine, constructionOverrides);
922
+ clearInsertParamOverrides(ctx.insertParamOverrides, target);
923
+ return { insertBaseResets: [target], constructionOverrides };
924
+ }
925
+
181
926
  export function setBusStripJson(ctx: EngineMixerContext, busId: number, sceneJson: string): void {
927
+ const target: StripJsonTarget = { kind: 'bus', busId };
928
+ // Parse before declaring a new bus so malformed text cannot declare one.
929
+ decodedInsertSignature(sceneJson, target);
182
930
  ctx.ensureBus(busId);
183
- ctx.offlineEngine.setBusStripJson(busId, sceneJson);
931
+ const { insertBaseResets, constructionOverrides } = replaceStripScene(
932
+ ctx,
933
+ target,
934
+ ctx.busStripJson.get(busId),
935
+ sceneJson,
936
+ );
937
+ pruneStripSidechains(ctx, target, sceneJson);
184
938
  ctx.busStripJson.set(busId, sceneJson);
185
- ctx.syncMixer();
939
+ ctx.syncMixer(insertBaseResets, constructionOverrides);
940
+ }
941
+
942
+ /** Applies a master strip scene while retaining native state for equal inserts. */
943
+ export function setMasterStripJson(ctx: EngineMixerContext, sceneJson: string): void {
944
+ const target: StripJsonTarget = { kind: 'master' };
945
+ const { insertBaseResets, constructionOverrides } = replaceStripScene(
946
+ ctx,
947
+ target,
948
+ ctx.getMasterStripJson(),
949
+ sceneJson,
950
+ );
951
+ pruneStripSidechains(ctx, target, sceneJson);
952
+ ctx.cacheMasterStripJson(sceneJson);
953
+ ctx.syncMixer(insertBaseResets, constructionOverrides);
186
954
  }