@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/AGENTS.md +20 -30
- package/corpus/templates/analytics/actions/search-dashboard-references.ts +58 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/analytics/app/lib/utils.ts +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +1 -1
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +2 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +366 -3
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +3 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -2
- package/corpus/templates/calendar/app/components/calendar/EventDialog.tsx +1 -1
- package/corpus/templates/calendar/app/lib/utils.ts +1 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +32 -5
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +15 -15
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
- package/corpus/templates/clips/app/lib/utils.ts +5 -6
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +43 -8
- package/corpus/templates/clips/desktop/src/app.tsx +26 -12
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +23 -9
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +261 -11
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +21 -3
- package/corpus/templates/clips/shared/mic-audio-cleanup.ts +350 -0
- package/corpus/templates/content/actions/resolve-content-landing.ts +162 -32
- package/corpus/templates/content/app/i18n-data.ts +10 -10
- package/corpus/templates/content/scripts/check-native-deps.mjs +181 -15
- package/corpus/templates/content/shared/content-landing.ts +5 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +79 -7
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +14 -2
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +2 -2
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +35 -11
- package/corpus/templates/design/actions/get-design-system.ts +103 -3
- package/corpus/templates/design/actions/import-design-tokens.ts +37 -8
- package/corpus/templates/design/actions/import-github.ts +32 -138
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +49 -16
- package/corpus/templates/design/actions/present-design-variants.ts +93 -4
- package/corpus/templates/design/actions/run-design-audit.ts +229 -0
- package/corpus/templates/design/actions/sync-design-system-with-builder.ts +64 -0
- package/corpus/templates/design/app/components/design/code-workbench/commands.ts +2 -2
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +17 -4
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +5 -1
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +5 -1
- package/corpus/templates/design/app/hooks/use-question-flow.ts +81 -5
- package/corpus/templates/design/app/i18n/en-US.ts +1 -1
- package/corpus/templates/design/app/i18n-data.ts +32 -1
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +10 -10
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +83 -11
- package/corpus/templates/design/app/pages/DesignSystems.tsx +14 -0
- package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +22 -0
- package/corpus/templates/design/changelog/2026-08-13-a-screen-that-would-render-behind-an-invisible-alpine-overla.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-attach-a-ui-screenshot-and-the-design-is-reproduced-not-rein.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-linking-a-design-system-means-you-never-get-generic-directio.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-reports-when-a-screen-ignores-its-linke.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-the-design-system-preview-now-shows-how-many-named-tokens-we.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-your-design-system-prompt-and-reference-screenshots-now-reac.md +6 -0
- package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +18 -2
- package/corpus/templates/design/shared/design-review.ts +1 -0
- package/corpus/templates/design/shared/html-integrity.ts +171 -0
- package/corpus/templates/mail/app/lib/utils.ts +1 -1
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +10 -6
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +41 -3
- package/corpus/templates/slides/actions/patch-deck.ts +12 -3
- package/corpus/templates/slides/actions/sync-design-system-with-builder.ts +64 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +193 -20
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +73 -1
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +64 -34
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +34 -41
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +119 -91
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +16 -0
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +15 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +158 -31
- package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +30 -14
- package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +4 -1
- package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +14 -0
- package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +24 -0
- package/corpus/templates/slides/app/context/DeckContext.tsx +19 -31
- package/corpus/templates/slides/app/global.css +24 -0
- package/corpus/templates/slides/app/hooks/use-navigation-state.ts +20 -17
- package/corpus/templates/slides/app/i18n/en-US.ts +19 -10
- package/corpus/templates/slides/app/lib/deck-filter.ts +47 -0
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +15 -4
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +637 -1
- package/corpus/templates/slides/app/lib/image-download.ts +84 -0
- package/corpus/templates/slides/app/lib/utils.ts +1 -1
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +9 -0
- package/corpus/templates/slides/app/pages/Index.tsx +25 -9
- package/corpus/templates/slides/changelog/2026-08-13-clicking-a-slide-thumbnail-now-focuses-it-so-the-slide-shortc.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-13-duplicating-or-restoring-a-slide-now-keeps-its-transition-ani.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-13-opening-presentation-mode-from-the-agent-now-starts-on-the-re.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-13-slide-transitions-can-be-set-from-a-visible-control-in-the-sl.md +6 -0
- package/corpus/templates/slides/netlify.toml +1 -0
- package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +14 -2
- package/dist/a2a/audience.d.ts +2 -0
- package/dist/a2a/audience.js +23 -0
- package/dist/a2a/client.d.ts +4 -0
- package/dist/a2a/client.js +32 -14
- package/dist/a2a/server.d.ts +9 -4
- package/dist/a2a/server.js +52 -25
- package/dist/a2a/types.d.ts +4 -4
- package/dist/agent/production-agent.js +1 -1
- package/dist/brand-kit/types.d.ts +10 -2
- package/dist/cli/code.js +1 -1
- package/dist/cli/index.js +30 -1
- package/dist/cli/native-dependencies.d.ts +24 -0
- package/dist/cli/native-dependencies.js +264 -0
- package/dist/cli/plan-local.js +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.js +2 -2
- package/dist/cli/visualize-repo.js +1 -1
- package/dist/client/FeedbackButton.js +12 -12
- package/dist/client/agent-chat-adapter.js +3 -4
- package/dist/client/assistant-ui-recovery.d.ts +1 -1
- package/dist/client/assistant-ui-recovery.js +4 -0
- package/dist/client/chat/message-components.d.ts +11 -3
- package/dist/client/chat/message-components.js +31 -5
- package/dist/client/chat/tool-call-display.d.ts +2 -1
- package/dist/client/chat/tool-call-display.js +33 -15
- package/dist/client/db-admin/SqlEditor.js +1 -1
- package/dist/client/dev-overlay/DevOverlay.js +2 -2
- package/dist/client/extensions/portable-extension.js +4 -0
- package/dist/client/guided-questions.d.ts +8 -0
- package/dist/client/guided-questions.js +12 -3
- package/dist/client/sharing/ShareButton.js +16 -24
- package/dist/client/sse-event-processor.js +35 -15
- package/dist/client/tool-display.d.ts +9 -0
- package/dist/client/tool-display.js +39 -0
- package/dist/client/vite-dev-recovery-script.js +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/html-shell.js +5 -0
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
- package/dist/server/agent-capabilities.js +0 -0
- package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -13
- package/dist/server/agent-chat/action-filters-a2a.js +59 -23
- package/dist/server/agent-chat/prompt-resources.js +17 -16
- package/dist/server/agent-chat-plugin.d.ts +9 -0
- package/dist/server/agent-chat-plugin.js +29 -7
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/builder-design-systems.d.ts +33 -1
- package/dist/server/builder-design-systems.js +293 -11
- package/dist/server/design-token-utils.d.ts +11 -0
- package/dist/server/design-token-utils.js +68 -16
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- 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
|
-
|
|
206
|
+
_capture_system_audio: bool,
|
|
207
207
|
) -> bool {
|
|
208
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
2562
|
-
|
|
2563
|
-
|
|
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 [
|
|
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.
|
|
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!(
|
|
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
|
}
|