@agent-native/core 0.153.10 → 0.154.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 (149) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/AGENTS.md +20 -30
  3. package/corpus/templates/analytics/actions/search-dashboard-references.ts +58 -0
  4. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  5. package/corpus/templates/analytics/app/lib/utils.ts +1 -1
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +1 -1
  7. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +2 -0
  8. package/corpus/templates/analytics/server/lib/dashboards-store.ts +366 -3
  9. package/corpus/templates/analytics/server/lib/real-data-actions.ts +3 -1
  10. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -2
  11. package/corpus/templates/calendar/app/components/calendar/EventDialog.tsx +1 -1
  12. package/corpus/templates/calendar/app/lib/utils.ts +1 -1
  13. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +32 -5
  14. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +15 -15
  15. package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
  16. package/corpus/templates/clips/app/lib/utils.ts +5 -6
  17. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +43 -8
  18. package/corpus/templates/clips/desktop/src/app.tsx +26 -12
  19. package/corpus/templates/clips/desktop/src/lib/recorder.ts +23 -9
  20. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +261 -11
  21. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +21 -3
  22. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +350 -0
  23. package/corpus/templates/content/actions/resolve-content-landing.ts +162 -32
  24. package/corpus/templates/content/app/i18n-data.ts +10 -10
  25. package/corpus/templates/content/scripts/check-native-deps.mjs +181 -15
  26. package/corpus/templates/content/shared/content-landing.ts +5 -0
  27. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +79 -7
  28. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +14 -2
  29. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +2 -2
  30. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +35 -11
  31. package/corpus/templates/design/actions/get-design-system.ts +103 -3
  32. package/corpus/templates/design/actions/import-design-tokens.ts +37 -8
  33. package/corpus/templates/design/actions/import-github.ts +32 -138
  34. package/corpus/templates/design/actions/index-design-system-with-builder.ts +49 -16
  35. package/corpus/templates/design/actions/present-design-variants.ts +93 -4
  36. package/corpus/templates/design/actions/run-design-audit.ts +229 -0
  37. package/corpus/templates/design/actions/sync-design-system-with-builder.ts +64 -0
  38. package/corpus/templates/design/app/components/design/code-workbench/commands.ts +2 -2
  39. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +17 -4
  40. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +5 -1
  41. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +5 -1
  42. package/corpus/templates/design/app/hooks/use-question-flow.ts +81 -5
  43. package/corpus/templates/design/app/i18n/en-US.ts +1 -1
  44. package/corpus/templates/design/app/i18n-data.ts +32 -1
  45. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +10 -10
  46. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +83 -11
  47. package/corpus/templates/design/app/pages/DesignSystems.tsx +14 -0
  48. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +22 -0
  49. package/corpus/templates/design/changelog/2026-08-13-a-screen-that-would-render-behind-an-invisible-alpine-overla.md +6 -0
  50. package/corpus/templates/design/changelog/2026-08-13-attach-a-ui-screenshot-and-the-design-is-reproduced-not-rein.md +6 -0
  51. package/corpus/templates/design/changelog/2026-08-13-linking-a-design-system-means-you-never-get-generic-directio.md +6 -0
  52. package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-reports-when-a-screen-ignores-its-linke.md +6 -0
  53. package/corpus/templates/design/changelog/2026-08-13-the-design-system-preview-now-shows-how-many-named-tokens-we.md +6 -0
  54. package/corpus/templates/design/changelog/2026-08-13-your-design-system-prompt-and-reference-screenshots-now-reac.md +6 -0
  55. package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +18 -2
  56. package/corpus/templates/design/shared/design-review.ts +1 -0
  57. package/corpus/templates/design/shared/html-integrity.ts +171 -0
  58. package/corpus/templates/mail/app/lib/utils.ts +1 -1
  59. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +10 -6
  60. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +41 -3
  61. package/corpus/templates/slides/actions/patch-deck.ts +12 -3
  62. package/corpus/templates/slides/actions/sync-design-system-with-builder.ts +64 -0
  63. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
  64. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +193 -20
  65. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +73 -1
  66. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +64 -34
  67. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +34 -41
  68. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +119 -91
  69. package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +16 -0
  70. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +15 -0
  71. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +158 -31
  72. package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +30 -14
  73. package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +2 -2
  74. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +4 -1
  75. package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +14 -0
  76. package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +24 -0
  77. package/corpus/templates/slides/app/context/DeckContext.tsx +19 -31
  78. package/corpus/templates/slides/app/global.css +24 -0
  79. package/corpus/templates/slides/app/hooks/use-navigation-state.ts +20 -17
  80. package/corpus/templates/slides/app/i18n/en-US.ts +19 -10
  81. package/corpus/templates/slides/app/lib/deck-filter.ts +47 -0
  82. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +15 -4
  83. package/corpus/templates/slides/app/lib/export-pptx-client.ts +637 -1
  84. package/corpus/templates/slides/app/lib/image-download.ts +84 -0
  85. package/corpus/templates/slides/app/lib/utils.ts +1 -1
  86. package/corpus/templates/slides/app/pages/DeckEditor.tsx +9 -0
  87. package/corpus/templates/slides/app/pages/Index.tsx +25 -9
  88. package/corpus/templates/slides/changelog/2026-08-13-clicking-a-slide-thumbnail-now-focuses-it-so-the-slide-shortc.md +6 -0
  89. package/corpus/templates/slides/changelog/2026-08-13-duplicating-or-restoring-a-slide-now-keeps-its-transition-ani.md +6 -0
  90. package/corpus/templates/slides/changelog/2026-08-13-opening-presentation-mode-from-the-agent-now-starts-on-the-re.md +6 -0
  91. package/corpus/templates/slides/changelog/2026-08-13-slide-transitions-can-be-set-from-a-visible-control-in-the-sl.md +6 -0
  92. package/corpus/templates/slides/netlify.toml +1 -0
  93. package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +14 -2
  94. package/dist/a2a/audience.d.ts +2 -0
  95. package/dist/a2a/audience.js +23 -0
  96. package/dist/a2a/client.d.ts +4 -0
  97. package/dist/a2a/client.js +32 -14
  98. package/dist/a2a/server.d.ts +9 -4
  99. package/dist/a2a/server.js +52 -25
  100. package/dist/a2a/types.d.ts +4 -4
  101. package/dist/agent/production-agent.js +1 -1
  102. package/dist/brand-kit/types.d.ts +10 -2
  103. package/dist/cli/code.js +1 -1
  104. package/dist/cli/index.js +30 -1
  105. package/dist/cli/native-dependencies.d.ts +24 -0
  106. package/dist/cli/native-dependencies.js +264 -0
  107. package/dist/cli/plan-local.js +1 -1
  108. package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
  109. package/dist/cli/skills-content/design-visual-edit-skill.js +2 -2
  110. package/dist/cli/visualize-repo.js +1 -1
  111. package/dist/client/FeedbackButton.js +12 -12
  112. package/dist/client/agent-chat-adapter.js +3 -4
  113. package/dist/client/assistant-ui-recovery.d.ts +1 -1
  114. package/dist/client/assistant-ui-recovery.js +4 -0
  115. package/dist/client/chat/message-components.d.ts +11 -3
  116. package/dist/client/chat/message-components.js +31 -5
  117. package/dist/client/chat/tool-call-display.d.ts +2 -1
  118. package/dist/client/chat/tool-call-display.js +33 -15
  119. package/dist/client/db-admin/SqlEditor.js +1 -1
  120. package/dist/client/dev-overlay/DevOverlay.js +2 -2
  121. package/dist/client/extensions/portable-extension.js +4 -0
  122. package/dist/client/guided-questions.d.ts +8 -0
  123. package/dist/client/guided-questions.js +12 -3
  124. package/dist/client/sharing/ShareButton.js +16 -24
  125. package/dist/client/sse-event-processor.js +35 -15
  126. package/dist/client/tool-display.d.ts +9 -0
  127. package/dist/client/tool-display.js +39 -0
  128. package/dist/client/vite-dev-recovery-script.js +1 -1
  129. package/dist/collab/awareness.d.ts +2 -2
  130. package/dist/collab/routes.d.ts +1 -1
  131. package/dist/collab/struct-routes.d.ts +1 -1
  132. package/dist/extensions/html-shell.js +5 -0
  133. package/dist/notifications/routes.d.ts +1 -1
  134. package/dist/observability/routes.d.ts +3 -3
  135. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  136. package/dist/server/agent-capabilities.js +0 -0
  137. package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -13
  138. package/dist/server/agent-chat/action-filters-a2a.js +59 -23
  139. package/dist/server/agent-chat/prompt-resources.js +17 -16
  140. package/dist/server/agent-chat-plugin.d.ts +9 -0
  141. package/dist/server/agent-chat-plugin.js +29 -7
  142. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  143. package/dist/server/builder-design-systems.d.ts +33 -1
  144. package/dist/server/builder-design-systems.js +293 -11
  145. package/dist/server/design-token-utils.d.ts +11 -0
  146. package/dist/server/design-token-utils.js +68 -16
  147. package/dist/server/index.d.ts +1 -1
  148. package/dist/server/index.js +1 -1
  149. package/package.json +2 -2
@@ -203,9 +203,13 @@ fn segmented_output_enabled(output: CustomWriterOutput, live_upload_enabled: boo
203
203
  fn live_audio_mixing_enabled(
204
204
  output: CustomWriterOutput,
205
205
  include_audio: bool,
206
- capture_system_audio: bool,
206
+ _capture_system_audio: bool,
207
207
  ) -> bool {
208
- include_audio && capture_system_audio && !output.preserves_separate_audio()
208
+ // Keep every ordinary microphone capture on the realtime mixer so the
209
+ // live-upload path receives the same mic cleanup as finalized files. A
210
+ // system-only capture has no mic signal to clean and keeps its original
211
+ // stereo track; Rewind deliberately preserves separate source tracks.
212
+ include_audio && !output.preserves_separate_audio()
209
213
  }
210
214
 
211
215
  pub(crate) fn audio_sidecar_path(video_path: &Path, source: &str) -> PathBuf {
@@ -1508,14 +1512,19 @@ impl CustomScreenCaptureWriter {
1508
1512
 
1509
1513
  // AVAssetWriter's fragmented profiles reject two independent AAC
1510
1514
  // inputs. Rewind therefore writes video-only fMP4 plus local PCM
1511
- // sidecars; ordinary recordings retain their existing inputs.
1515
+ // sidecars; ordinary recordings retain a single mixed input when
1516
+ // a microphone is present, including mic-only recordings.
1512
1517
  let (system_audio_input, mic_audio_input, mixed_audio_input, mixer) = if mix_live {
1513
1518
  let mixed = av_make_audio_writer_input(input_cls, &writer)?;
1514
1519
  (
1515
1520
  None,
1516
1521
  None,
1517
1522
  Some(mixed),
1518
- Some(LiveAudioMixer::new(segmented)?),
1523
+ Some(LiveAudioMixer::new(
1524
+ segmented,
1525
+ include_audio,
1526
+ capture_system_audio,
1527
+ )?),
1519
1528
  )
1520
1529
  } else {
1521
1530
  let system_audio_input =
@@ -2173,6 +2182,18 @@ const MIC_AGC_BOOST_SECONDS: f32 = 1.5;
2173
2182
  const MIC_AGC_WARMUP_SECONDS: f32 = 0.2;
2174
2183
  /// How close to the target counts as levelled and ends the warmup.
2175
2184
  const MIC_AGC_CONVERGED_TOLERANCE: f32 = 0.05;
2185
+ /// Conservative voice cleanup for the realtime microphone path. The browser
2186
+ /// recorder gets a comparable Web Audio chain; native live uploads cannot
2187
+ /// wait for the later FFmpeg normalization pass, so this stays causal and
2188
+ /// source-local here.
2189
+ const MIC_CLEANUP_HIGH_PASS_HZ: f32 = 65.0;
2190
+ const MIC_CLEANUP_NOTCH_FREQUENCIES_HZ: [f32; 4] = [50.0, 60.0, 100.0, 120.0];
2191
+ const MIC_CLEANUP_NOTCH_Q: f32 = 18.0;
2192
+ const MIC_EXPANDER_THRESHOLD_DB: f32 = -48.0;
2193
+ const MIC_EXPANDER_RATIO: f32 = 2.0;
2194
+ const MIC_EXPANDER_MAX_REDUCTION_DB: f32 = 12.0;
2195
+ const MIC_EXPANDER_ATTACK_SECONDS: f32 = 0.012;
2196
+ const MIC_EXPANDER_RELEASE_SECONDS: f32 = 0.18;
2176
2197
  /// -1.0 dBFS, near the true-peak ceiling the offline loudnorm chain targets.
2177
2198
  const MIX_LIMIT_CEILING: f32 = 0.891;
2178
2199
  const MIX_LIMIT_RELEASE_SECONDS: f32 = 0.15;
@@ -2254,6 +2275,156 @@ impl MicAutoGain {
2254
2275
  }
2255
2276
  }
2256
2277
 
2278
+ #[derive(Clone, Copy)]
2279
+ struct Biquad {
2280
+ b0: f32,
2281
+ b1: f32,
2282
+ b2: f32,
2283
+ a1: f32,
2284
+ a2: f32,
2285
+ x1: f32,
2286
+ x2: f32,
2287
+ y1: f32,
2288
+ y2: f32,
2289
+ }
2290
+
2291
+ impl Biquad {
2292
+ fn from_coefficients(b0: f32, b1: f32, b2: f32, a0: f32, a1: f32, a2: f32) -> Self {
2293
+ Self {
2294
+ b0: b0 / a0,
2295
+ b1: b1 / a0,
2296
+ b2: b2 / a0,
2297
+ a1: a1 / a0,
2298
+ a2: a2 / a0,
2299
+ x1: 0.0,
2300
+ x2: 0.0,
2301
+ y1: 0.0,
2302
+ y2: 0.0,
2303
+ }
2304
+ }
2305
+
2306
+ fn high_pass(frequency_hz: f32, q: f32, sample_rate: f32) -> Self {
2307
+ let omega = 2.0 * std::f32::consts::PI * frequency_hz / sample_rate;
2308
+ let cosine = omega.cos();
2309
+ let sine = omega.sin();
2310
+ let alpha = sine / (2.0 * q);
2311
+ Self::from_coefficients(
2312
+ (1.0 + cosine) / 2.0,
2313
+ -(1.0 + cosine),
2314
+ (1.0 + cosine) / 2.0,
2315
+ 1.0 + alpha,
2316
+ -2.0 * cosine,
2317
+ 1.0 - alpha,
2318
+ )
2319
+ }
2320
+
2321
+ fn notch(frequency_hz: f32, q: f32, sample_rate: f32) -> Self {
2322
+ let omega = 2.0 * std::f32::consts::PI * frequency_hz / sample_rate;
2323
+ let cosine = omega.cos();
2324
+ let sine = omega.sin();
2325
+ let alpha = sine / (2.0 * q);
2326
+ Self::from_coefficients(
2327
+ 1.0,
2328
+ -2.0 * cosine,
2329
+ 1.0,
2330
+ 1.0 + alpha,
2331
+ -2.0 * cosine,
2332
+ 1.0 - alpha,
2333
+ )
2334
+ }
2335
+
2336
+ fn process(&mut self, input: f32) -> f32 {
2337
+ let output = self.b0 * input + self.b1 * self.x1 + self.b2 * self.x2
2338
+ - self.a1 * self.y1
2339
+ - self.a2 * self.y2;
2340
+ self.x2 = self.x1;
2341
+ self.x1 = input;
2342
+ self.y2 = self.y1;
2343
+ self.y1 = output;
2344
+ output
2345
+ }
2346
+ }
2347
+
2348
+ struct MicCleanupChannel {
2349
+ high_pass: Biquad,
2350
+ notches: [Biquad; 4],
2351
+ }
2352
+
2353
+ impl MicCleanupChannel {
2354
+ fn new(sample_rate: f32) -> Self {
2355
+ Self {
2356
+ high_pass: Biquad::high_pass(MIC_CLEANUP_HIGH_PASS_HZ, 0.707, sample_rate),
2357
+ notches: std::array::from_fn(|index| {
2358
+ Biquad::notch(
2359
+ MIC_CLEANUP_NOTCH_FREQUENCIES_HZ[index],
2360
+ MIC_CLEANUP_NOTCH_Q,
2361
+ sample_rate,
2362
+ )
2363
+ }),
2364
+ }
2365
+ }
2366
+
2367
+ fn process(&mut self, mut sample: f32) -> f32 {
2368
+ sample = self.high_pass.process(sample);
2369
+ for notch in &mut self.notches {
2370
+ sample = notch.process(sample);
2371
+ }
2372
+ sample
2373
+ }
2374
+ }
2375
+
2376
+ /// Causal microphone cleanup for the native live-upload path. The high-pass
2377
+ /// and narrow notches target HVAC/mains hum; the expander only reduces quiet
2378
+ /// room tone and never hard-mutes it, so speech tails survive the processing.
2379
+ struct MicCleanup {
2380
+ left: MicCleanupChannel,
2381
+ right: MicCleanupChannel,
2382
+ envelope: f32,
2383
+ gain: f32,
2384
+ envelope_coefficient: f32,
2385
+ attack_coefficient: f32,
2386
+ release_coefficient: f32,
2387
+ }
2388
+
2389
+ impl MicCleanup {
2390
+ fn new(sample_rate: i32) -> Self {
2391
+ let sample_rate = sample_rate as f32;
2392
+ Self {
2393
+ left: MicCleanupChannel::new(sample_rate),
2394
+ right: MicCleanupChannel::new(sample_rate),
2395
+ envelope: 0.0,
2396
+ gain: 1.0,
2397
+ envelope_coefficient: one_pole_coefficient(0.015, sample_rate as i32),
2398
+ attack_coefficient: one_pole_coefficient(
2399
+ MIC_EXPANDER_ATTACK_SECONDS,
2400
+ sample_rate as i32,
2401
+ ),
2402
+ release_coefficient: one_pole_coefficient(
2403
+ MIC_EXPANDER_RELEASE_SECONDS,
2404
+ sample_rate as i32,
2405
+ ),
2406
+ }
2407
+ }
2408
+
2409
+ fn process(&mut self, sample: (f32, f32)) -> (f32, f32) {
2410
+ let filtered = (self.left.process(sample.0), self.right.process(sample.1));
2411
+ let peak = filtered.0.abs().max(filtered.1.abs());
2412
+ self.envelope += (peak - self.envelope) * self.envelope_coefficient;
2413
+ let level_db = 20.0 * self.envelope.max(1.0e-6).log10();
2414
+ let below_threshold = (MIC_EXPANDER_THRESHOLD_DB - level_db).max(0.0);
2415
+ let reduction_db =
2416
+ (below_threshold * (1.0 - 1.0 / MIC_EXPANDER_RATIO)).min(MIC_EXPANDER_MAX_REDUCTION_DB);
2417
+ let target_gain = 10.0_f32.powf(-reduction_db / 20.0);
2418
+ let coefficient = if target_gain > self.gain {
2419
+ self.attack_coefficient
2420
+ } else {
2421
+ self.release_coefficient
2422
+ };
2423
+ self.gain += (target_gain - self.gain) * coefficient;
2424
+ (filtered.0 * self.gain, filtered.1 * self.gain)
2425
+ }
2426
+ }
2427
+
2257
2428
  /// Zero-latency peak limiter over the summed mix.
2258
2429
  ///
2259
2430
  /// This replaces a flat 0.5x weight on each source, which bought clip safety
@@ -2287,6 +2458,7 @@ impl PeakLimiter {
2287
2458
  /// is reachable without constructing a `CMFormatDescription` — the reported bug
2288
2459
  /// was a wrong per-frame weight, so that arithmetic is what needs covering.
2289
2460
  struct MixGainStage {
2461
+ mic_cleanup: MicCleanup,
2290
2462
  mic: MicAutoGain,
2291
2463
  limiter: PeakLimiter,
2292
2464
  }
@@ -2294,6 +2466,7 @@ struct MixGainStage {
2294
2466
  impl MixGainStage {
2295
2467
  fn new(sample_rate: i32) -> Self {
2296
2468
  Self {
2469
+ mic_cleanup: MicCleanup::new(sample_rate),
2297
2470
  mic: MicAutoGain::new(sample_rate),
2298
2471
  limiter: PeakLimiter::new(sample_rate),
2299
2472
  }
@@ -2303,6 +2476,7 @@ impl MixGainStage {
2303
2476
  /// at unity and the mic is only ever boosted, with the limiter owning peak
2304
2477
  /// safety for the sum.
2305
2478
  fn frame(&mut self, system: (f32, f32), mic: (f32, f32)) -> (f32, f32) {
2479
+ let mic = self.mic_cleanup.process(mic);
2306
2480
  let mic_gain = self.mic.next_gain(mic.0, mic.1);
2307
2481
  let left = system.0 + mic.0 * mic_gain;
2308
2482
  let right = system.1 + mic.1 * mic_gain;
@@ -2411,6 +2585,8 @@ enum SourceBound {
2411
2585
  struct LiveAudioMixer {
2412
2586
  format_desc: screencapturekit::cm::CMFormatDescription,
2413
2587
  sample_rate: i32,
2588
+ include_microphone: bool,
2589
+ capture_system_audio: bool,
2414
2590
  /// Subtract the writer session start from emitted PTS (segmented mode,
2415
2591
  /// where the session timeline is zero-based). Plain file mode keeps
2416
2592
  /// absolute source time to match `startSessionAtSourceTime:`.
@@ -2432,7 +2608,11 @@ struct LiveAudioMixer {
2432
2608
  }
2433
2609
 
2434
2610
  impl LiveAudioMixer {
2435
- fn new(rebase_output: bool) -> Result<Self, String> {
2611
+ fn new(
2612
+ rebase_output: bool,
2613
+ include_microphone: bool,
2614
+ capture_system_audio: bool,
2615
+ ) -> Result<Self, String> {
2436
2616
  let sample_rate = AUDIO_OUTPUT_SAMPLE_RATE as i32;
2437
2617
  let asbd = AudioStreamBasicDescription {
2438
2618
  sample_rate: AUDIO_OUTPUT_SAMPLE_RATE as f64,
@@ -2468,6 +2648,8 @@ impl LiveAudioMixer {
2468
2648
  Ok(Self {
2469
2649
  format_desc,
2470
2650
  sample_rate,
2651
+ include_microphone,
2652
+ capture_system_audio,
2471
2653
  rebase_output,
2472
2654
  session_start_seconds: None,
2473
2655
  pause_offset_seconds: 0.0,
@@ -2555,18 +2737,35 @@ impl LiveAudioMixer {
2555
2737
  /// active sources (or everything buffered when flushing at stop).
2556
2738
  fn compute_safe_end(&self, flush: bool) -> i64 {
2557
2739
  if flush {
2558
- return self.system.end_frame().max(self.mic.end_frame());
2740
+ let mut end = self.out_pos;
2741
+ if self.capture_system_audio {
2742
+ end = end.max(self.system.end_frame());
2743
+ }
2744
+ if self.include_microphone {
2745
+ end = end.max(self.mic.end_frame());
2746
+ }
2747
+ return end;
2559
2748
  }
2560
2749
  let now = Instant::now();
2561
- let sys = self.classify(&self.system, now);
2562
- let mic = self.classify(&self.mic, now);
2563
- if matches!(sys, SourceBound::Pending) || matches!(mic, SourceBound::Pending) {
2750
+ let sys = self
2751
+ .capture_system_audio
2752
+ .then(|| self.classify(&self.system, now));
2753
+ let mic = self
2754
+ .include_microphone
2755
+ .then(|| self.classify(&self.mic, now));
2756
+ if sys
2757
+ .as_ref()
2758
+ .is_some_and(|bound| matches!(bound, SourceBound::Pending))
2759
+ || mic
2760
+ .as_ref()
2761
+ .is_some_and(|bound| matches!(bound, SourceBound::Pending))
2762
+ {
2564
2763
  // Still expecting a source to start; don't run ahead of it.
2565
2764
  return self.out_pos;
2566
2765
  }
2567
2766
  let mut bound = i64::MAX;
2568
2767
  let mut any_active = false;
2569
- for b in [&sys, &mic] {
2768
+ for b in [sys.as_ref(), mic.as_ref()].into_iter().flatten() {
2570
2769
  if let SourceBound::Active(end) = b {
2571
2770
  bound = bound.min(*end);
2572
2771
  any_active = true;
@@ -2576,7 +2775,14 @@ impl LiveAudioMixer {
2576
2775
  bound
2577
2776
  } else {
2578
2777
  // Everything stalled/absent: drain whatever frozen data we have.
2579
- self.system.end_frame().max(self.mic.end_frame())
2778
+ let mut end = self.out_pos;
2779
+ if self.capture_system_audio {
2780
+ end = end.max(self.system.end_frame());
2781
+ }
2782
+ if self.include_microphone {
2783
+ end = end.max(self.mic.end_frame());
2784
+ }
2785
+ end
2580
2786
  }
2581
2787
  }
2582
2788
 
@@ -4116,6 +4322,16 @@ mod fragment_fence_tests {
4116
4322
  true,
4117
4323
  true
4118
4324
  ));
4325
+ assert!(live_audio_mixing_enabled(
4326
+ CustomWriterOutput::Standard,
4327
+ true,
4328
+ false
4329
+ ));
4330
+ assert!(!live_audio_mixing_enabled(
4331
+ CustomWriterOutput::Standard,
4332
+ false,
4333
+ true
4334
+ ));
4119
4335
  // Clip HLS is forced segmented regardless of the ordinary remote flag,
4120
4336
  // but keeps a single live-mixed AAC track and no Rewind sidecars.
4121
4337
  assert!(segmented_output_enabled(CustomWriterOutput::ClipHls, false));
@@ -4126,6 +4342,40 @@ mod fragment_fence_tests {
4126
4342
  ));
4127
4343
  }
4128
4344
 
4345
+ #[test]
4346
+ fn mic_cleanup_reduces_hum_without_hard_muting_speech() {
4347
+ let sample_rate = 48_000.0_f32;
4348
+ let mut cleanup = MicCleanup::new(sample_rate as i32);
4349
+ let mut hum_energy = 0.0_f32;
4350
+ let mut speech_energy = 0.0_f32;
4351
+ for index in 0..(sample_rate as usize) {
4352
+ let time = index as f32 / sample_rate;
4353
+ let hum = (2.0 * std::f32::consts::PI * 60.0 * time).sin() * 0.08;
4354
+ let speech = if (0.2..0.8).contains(&time) {
4355
+ (2.0 * std::f32::consts::PI * 220.0 * time).sin() * 0.08
4356
+ } else {
4357
+ 0.0
4358
+ };
4359
+ let output = cleanup.process((hum + speech, hum + speech));
4360
+ if time < 0.15 || time > 0.85 {
4361
+ hum_energy += output.0.abs();
4362
+ } else {
4363
+ speech_energy += output.0.abs();
4364
+ }
4365
+ }
4366
+ assert!(hum_energy < 900.0, "hum remained too loud: {hum_energy}");
4367
+ assert!(
4368
+ speech_energy > 1_000.0,
4369
+ "speech was gated out: {speech_energy}"
4370
+ );
4371
+ }
4372
+
4373
+ #[test]
4374
+ fn mic_only_mixer_does_not_wait_for_a_disabled_system_source() {
4375
+ let mixer = LiveAudioMixer::new(false, true, false).unwrap();
4376
+ assert_eq!(mixer.compute_safe_end(false), 0);
4377
+ }
4378
+
4129
4379
  #[test]
4130
4380
  fn clip_sink_gate_orders_prepare_activate_and_logical_close() {
4131
4381
  let gate = ClipSinkGate::new();
@@ -64,11 +64,22 @@ const AUDIO_SIGNAL_MIN_MAX_VOLUME_DB: f64 = -30.0;
64
64
  // stereo content that must not be flattened.
65
65
  const AUDIO_DOWNMIX_FILTER: &str =
66
66
  "aformat=channel_layouts=stereo,pan=stereo|FL=0.5*FL+0.5*FR|FR=0.5*FL+0.5*FR";
67
+ // The legacy ScreenCaptureKit output carries mic + system audio together on
68
+ // the two source channels. Remove only the narrow mains tones before the
69
+ // centered downmix; do not apply the mic-only high-pass/expander to system
70
+ // audio, which may legitimately contain bass.
71
+ const AUDIO_MIXED_HUM_FILTER: &str =
72
+ "bandreject=f=50:t=q:w=18,bandreject=f=60:t=q:w=18,bandreject=f=100:t=q:w=18,bandreject=f=120:t=q:w=18";
67
73
  // Mild FFT denoise for native mic captures. Browser recordings already request
68
74
  // WebRTC noise suppression; ScreenCaptureKit/screencapture mic capture does
69
75
  // not, so reduce steady broadband room/mic hiss during the existing optimize
70
76
  // step. Keep this conservative to avoid watery speech artifacts.
71
77
  const AUDIO_DENOISE_FILTER: &str = "afftdn=nr=10:nf=-50:tn=1";
78
+ // The FFT pass above is intentionally broad and does not reliably remove a
79
+ // tonal HVAC hum. Apply this narrower chain only to mic-only captures so
80
+ // system audio/music keeps its low end when both sources are recorded.
81
+ const AUDIO_MIC_CLEANUP_FILTER: &str =
82
+ "highpass=f=65,bandreject=f=50:t=q:w=18,bandreject=f=60:t=q:w=18,bandreject=f=100:t=q:w=18,bandreject=f=120:t=q:w=18,agate=threshold=0.008:ratio=2:range=0.35:attack=10:release=180:knee=4:detection=rms";
72
83
  // loudnorm operates internally at 192 kHz and emits at 192 kHz; without an
73
84
  // explicit output rate the AAC track ends up at 192 kHz and plays back slow.
74
85
  const AUDIO_OUTPUT_SAMPLE_RATE: u32 = 48000;
@@ -92,11 +103,15 @@ const AUDIO_DOWNMIX_MAKEUP_FILTER: &str = "volume=6dB";
92
103
  fn audio_filter_chain(downmix: bool, denoise: bool, mic_pregain: bool) -> String {
93
104
  let mut filters = Vec::new();
94
105
  if downmix {
106
+ filters.push(AUDIO_MIXED_HUM_FILTER);
95
107
  filters.push(AUDIO_DOWNMIX_FILTER);
96
108
  // Undo pan attenuation; do not also apply mic-only pregain here —
97
109
  // system audio would get a double boost.
98
110
  filters.push(AUDIO_DOWNMIX_MAKEUP_FILTER);
99
111
  }
112
+ if mic_pregain {
113
+ filters.push(AUDIO_MIC_CLEANUP_FILTER);
114
+ }
100
115
  if denoise {
101
116
  filters.push(AUDIO_DENOISE_FILTER);
102
117
  }
@@ -8161,7 +8176,8 @@ mod audio_track_probe_tests {
8161
8176
  audio_filter_chain, decide_prepared_audio_signal, mp4_has_audio_track,
8162
8177
  parse_ffmpeg_volume_db, AudioSignalProbe, PreparedAudioSignalDecision,
8163
8178
  AUDIO_DENOISE_FILTER, AUDIO_DOWNMIX_FILTER, AUDIO_DOWNMIX_MAKEUP_FILTER,
8164
- AUDIO_LOUDNESS_FILTER, AUDIO_MIC_PREGAIN_FILTER,
8179
+ AUDIO_LOUDNESS_FILTER, AUDIO_MIC_CLEANUP_FILTER, AUDIO_MIC_PREGAIN_FILTER,
8180
+ AUDIO_MIXED_HUM_FILTER,
8165
8181
  };
8166
8182
  use std::io::Write;
8167
8183
 
@@ -8214,12 +8230,14 @@ mod audio_track_probe_tests {
8214
8230
  );
8215
8231
  assert_eq!(
8216
8232
  audio_filter_chain(false, true, true),
8217
- format!("{AUDIO_DENOISE_FILTER},{AUDIO_MIC_PREGAIN_FILTER},{AUDIO_LOUDNESS_FILTER}")
8233
+ format!(
8234
+ "{AUDIO_MIC_CLEANUP_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_MIC_PREGAIN_FILTER},{AUDIO_LOUDNESS_FILTER}"
8235
+ )
8218
8236
  );
8219
8237
  assert_eq!(
8220
8238
  audio_filter_chain(true, true, false),
8221
8239
  format!(
8222
- "{AUDIO_DOWNMIX_FILTER},{AUDIO_DOWNMIX_MAKEUP_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}"
8240
+ "{AUDIO_MIXED_HUM_FILTER},{AUDIO_DOWNMIX_FILTER},{AUDIO_DOWNMIX_MAKEUP_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}"
8223
8241
  )
8224
8242
  );
8225
8243
  }