fix(audio): both sync offsets ship at zero

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.
This commit is contained in:
2026-11-12 14:16:20 -08:00
parent c1b3868913
commit 1eb04597d1
4 changed files with 59 additions and 9 deletions
+3 -3
View File
@@ -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
+6 -3
View File
@@ -117,9 +117,12 @@ public partial class LayoutStore : IDisposable
return int.TryParse(value, out var ms) ? Math.Clamp(ms, -500, 500) : 0;
}
/// <summary>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.</summary>
public const int DefaultMicSyncOffsetMs = 175;
/// <summary>
/// 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.
/// </summary>
public const int DefaultMicSyncOffsetMs = 0;
/// <summary>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.</summary>
+3 -3
View File
@@ -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<int> 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
+47
View File
@@ -0,0 +1,47 @@
using System;
using System.IO;
using Microsoft.Data.Sqlite;
using Xunit;
using ytLive.Services;
namespace ytLive.Tests;
/// <summary>
/// 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.
/// </summary>
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());
}
}