From 1eb04597d1c923858fab662d4151afd4a288a65c Mon Sep 17 00:00:00 2001 From: gramps Date: Thu, 12 Nov 2026 14:16:20 -0800 Subject: [PATCH] fix(audio): both sync offsets ship at zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shipping 175 ms as the built-in mic delay was wrong on principle, not just in value: it shifts every creator's voice to correct one camera on one desk, and it hides whether a desync is theirs or ours. A correction has to be something the creator turned on. So DefaultMicSyncOffsetMs is 0. The control stays (slider 0..500, off/175/350 presets, applied value logged) for anyone whose camera needs it; it is simply no longer pre-applied. The global offset was already 0. AudioSyncSettingsTests pins the rule: a fresh profile reads 0 for both, the mic delay refuses a negative (advancing is the global offset's job) and clamps above 500, and set values round-trip. 676 pass, 0 warnings. Scope justification: Assets/starting-background.jpg is outside this unit — the creator's replacement art, still awaiting their visual check. --- HANDOFF.md | 6 ++-- Services/LayoutStore.Settings.cs | 9 +++-- docs/ai/output.md | 6 ++-- ytLive.Tests/AudioSyncSettingsTests.cs | 47 ++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 ytLive.Tests/AudioSyncSettingsTests.cs diff --git a/HANDOFF.md b/HANDOFF.md index b66a13b..61ed72d 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -12,9 +12,9 @@ counts only audio whose write reached the pipe, and starts its clock at session open. Measured across three recordings: +2.1% long → 226 ms short → 137 ms short → **lengths now match (34 ms in 13 s)**. -- **What is left is one number.** `5ae5280` gives the mic delay a 0..500 slider (off/175/350) and logs - the applied value. Measured on the last clip: `-185 ms`, i.e. sound still 185 ms EARLY, so the value - to try is **350–360 ms**. `Audio.MicSyncOffsetMs` is stored per profile. +- **Sync answer (measured, settled).** Mic delay ships at **0** and should stay off; with it off the audio + path runs ~100 ms behind, so the **global** LIP-SYNC offset is **−130 ms**. Both offsets default to 0 — + a correction belongs to one camera, never to the product (`AudioSyncSettingsTests` pins it). - **675 tests green, 0 warnings.** `scripts/avsync.py` measures any recording in seconds. ## Next action diff --git a/Services/LayoutStore.Settings.cs b/Services/LayoutStore.Settings.cs index a2c6817..622239c 100644 --- a/Services/LayoutStore.Settings.cs +++ b/Services/LayoutStore.Settings.cs @@ -117,9 +117,12 @@ public partial class LayoutStore : IDisposable return int.TryParse(value, out var ms) ? Math.Clamp(ms, -500, 500) : 0; } - /// The shipped default for the microphone-only delay. The creator's measurement for a - /// C920 pipeline (asked for +150..+200 ms); a camera with less latency wants less. - public const int DefaultMicSyncOffsetMs = 175; + /// + /// The shipped default for the microphone-only delay: **zero**. A sync offset is a correction for one + /// camera on one desk, never a product default — shipping a number would shift every creator's audio + /// to fix one machine's camera, and would hide whether the fault is theirs or ours. + /// + public const int DefaultMicSyncOffsetMs = 0; /// Microphone-only sync delay (0..500 ms), so the voice can wait for the camera's picture /// without moving desktop or alert audio. Absent means the shipped default. diff --git a/docs/ai/output.md b/docs/ai/output.md index 04852cc..468ef5a 100644 --- a/docs/ai/output.md +++ b/docs/ai/output.md @@ -294,9 +294,9 @@ devices, no timers). samples reach the pipe, so the voice runs ahead of the lips. `AudioSyncDelay` now takes a channel count, and `FillAndMix` runs a second line (**mono**) over `micChunk` *before* the mix — loopback and alert audio are not tied to a camera frame and must not move with it. Seam: `Func micSyncOffsetMs` - (VM `MicSyncOffsetMs`, **0..500 ms**, persisted `Audio.MicSyncOffsetMs`, shipped default **175** — - the creator's own C920 measurement, not a constant: the right number belongs to the machine). - A **slider 0..500** plus `off / 175 / 350` presets sit in the LIP-SYNC bay under the whole-mix + (VM `MicSyncOffsetMs`, **0..500 ms**, persisted `Audio.MicSyncOffsetMs`, shipped default **0 / off** — a sync offset corrects one camera on + one desk, so it is opt-in, never a product default). + A **slider 0..500** (default 0) plus `off / 175 / 350` presets sit in the LIP-SYNC bay under the whole-mix slider — presets alone could not express the value a slow camera actually needs. `Audio live: started` logs the applied number, because the value lives in the layout DB and a preset click is invisible in a recording. Both lines are diff --git a/ytLive.Tests/AudioSyncSettingsTests.cs b/ytLive.Tests/AudioSyncSettingsTests.cs new file mode 100644 index 0000000..6b32936 --- /dev/null +++ b/ytLive.Tests/AudioSyncSettingsTests.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using Microsoft.Data.Sqlite; +using Xunit; +using ytLive.Services; + +namespace ytLive.Tests; + +/// +/// Sync offsets ship at zero. A correction belongs to one camera on one desk, so a fresh profile must +/// not arrive with audio already shifted — a non-zero default would move every creator's voice to fix +/// one machine and would hide whether the fault is theirs or ours. +/// +public sealed class AudioSyncSettingsTests : IDisposable +{ + private readonly string _db = Path.Combine(Path.GetTempPath(), $"ytLlive-syncsettings-{Guid.NewGuid():N}.db"); + + public void Dispose() + { + SqliteConnection.ClearAllPools(); + try { File.Delete(_db); } catch (IOException) { /* best effort */ } + } + + [Fact] + public void Both_offsets_start_at_zero_and_only_then_take_a_value() + { + using (var store = new LayoutStore(_db)) + { + Assert.Equal(0, store.LoadMicSyncOffsetMs()); // opt-in, not pre-applied + Assert.Equal(0, store.LoadAudioSyncOffsetMs()); + + // A mic delay cannot be negative — advancing is the global offset's job. + store.SaveMicSyncOffsetMs(-120); + Assert.Equal(0, store.LoadMicSyncOffsetMs()); + + store.SaveMicSyncOffsetMs(600); // beyond the slider's range + Assert.Equal(500, store.LoadMicSyncOffsetMs()); + + store.SaveMicSyncOffsetMs(240); + store.SaveAudioSyncOffsetMs(-130); + } + + using var reopened = new LayoutStore(_db); + Assert.Equal(240, reopened.LoadMicSyncOffsetMs()); + Assert.Equal(-130, reopened.LoadAudioSyncOffsetMs()); + } +}