Fix audio clock drift and adaptive jitter buffering
This commit is contained in:
@@ -34,7 +34,7 @@ public sealed class WasapiAudioBackend : IAudioDeviceBackend
|
||||
private sealed class Playback : IAudioPlayback
|
||||
{
|
||||
private readonly string? deviceId;
|
||||
private readonly PcmRing pcm = new(32768);
|
||||
private readonly AdaptivePcmBuffer pcm = new(2);
|
||||
private readonly ManualResetEventSlim initialized = new(false);
|
||||
private readonly Thread thread;
|
||||
private volatile bool running = true;
|
||||
@@ -47,6 +47,7 @@ public sealed class WasapiAudioBackend : IAudioDeviceBackend
|
||||
thread.Start();
|
||||
if (!initialized.Wait(5000) || !ready) { Dispose(); throw new InvalidOperationException("WASAPI playback could not start."); }
|
||||
}
|
||||
public int BufferMilliseconds { get => pcm.BufferMilliseconds; set => pcm.BufferMilliseconds = value; }
|
||||
public void Write(ReadOnlySpan<short> stereoPcm) => pcm.TryWrite(stereoPcm);
|
||||
private unsafe void Work()
|
||||
{
|
||||
@@ -68,7 +69,6 @@ public sealed class WasapiAudioBackend : IAudioDeviceBackend
|
||||
render = (IAudioRenderClient)output;
|
||||
if (client.Start() < 0) return;
|
||||
ready = true; initialized.Set();
|
||||
Span<short> discard = stackalloc short[1920];
|
||||
while (running)
|
||||
{
|
||||
bufferReady.WaitOne(20); // Scheduling wait is outside the buffer-fill cycle.
|
||||
@@ -77,8 +77,6 @@ public sealed class WasapiAudioBackend : IAudioDeviceBackend
|
||||
if (frames == 0) continue;
|
||||
if (render.GetBuffer(frames, out nint buffer) < 0) break;
|
||||
var destination = new Span<short>((void*)buffer, checked((int)frames * 2));
|
||||
// Keep queued playback bounded to ~120 ms, then fill underflow with silence.
|
||||
while (pcm.Count > 11520) pcm.Read(discard);
|
||||
int count = pcm.Read(destination); destination[count..].Clear();
|
||||
if (render.ReleaseBuffer(frames, 0) < 0) break;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public sealed class AudioSettingsForm : Form
|
||||
private readonly bool _origAuxEnabled;
|
||||
private readonly string? _origAuxDeviceId;
|
||||
private readonly int _origAuxGain;
|
||||
private readonly int _origAudioBufferMilliseconds;
|
||||
|
||||
private readonly ComboBox _cboDevice;
|
||||
private readonly Button _btnRefresh;
|
||||
@@ -54,6 +55,7 @@ public sealed class AudioSettingsForm : Form
|
||||
private readonly Button _btnAuxRefresh;
|
||||
private readonly Label _lblAuxGain;
|
||||
private readonly TrackBar _trkAuxGain;
|
||||
private readonly ComboBox _cboBuffer;
|
||||
|
||||
private Keys _pttKey;
|
||||
|
||||
@@ -80,6 +82,7 @@ public sealed class AudioSettingsForm : Form
|
||||
_origAuxEnabled = settings.AuxEnabled;
|
||||
_origAuxDeviceId = settings.AuxDeviceId;
|
||||
_origAuxGain = settings.AuxGain;
|
||||
_origAudioBufferMilliseconds = settings.AudioBufferMilliseconds;
|
||||
|
||||
Text = "Audio settings";
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
@@ -87,7 +90,7 @@ public sealed class AudioSettingsForm : Form
|
||||
MinimizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(420, 639);
|
||||
ClientSize = new Size(420, 690);
|
||||
|
||||
// ── Device row ────────────────────────────────────────────────────────
|
||||
var lblDevice = new Label
|
||||
@@ -319,19 +322,34 @@ public sealed class AudioSettingsForm : Form
|
||||
"Volume of the aux input stream. 100 is unity gain; range 0–400 percent.";
|
||||
_trkAuxGain.Scroll += TrkAuxGain_Scroll;
|
||||
|
||||
var lblBuffer = new Label { Text = "Audio &buffering:", Location = new Point(12, 590), AutoSize = true };
|
||||
_cboBuffer = new ComboBox
|
||||
{
|
||||
Location = new Point(130, 586), Width = 190, DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
AccessibleName = "Audio buffering",
|
||||
AccessibleDescription = "Local capture and playback buffering. Lower values reduce latency; higher values tolerate more scheduling jitter."
|
||||
};
|
||||
_cboBuffer.Items.AddRange(["Low latency (20 ms)", "Balanced (40 ms)", "Stable (60 ms)"]);
|
||||
_cboBuffer.SelectedIndex = settings.AudioBufferMilliseconds switch { 20 => 0, 60 => 2, _ => 1 };
|
||||
_cboBuffer.SelectedIndexChanged += (_, _) =>
|
||||
{
|
||||
_settings.AudioBufferMilliseconds = _cboBuffer.SelectedIndex switch { 0 => 20, 2 => 60, _ => 40 };
|
||||
_client.SetAudioBufferMilliseconds(_settings.AudioBufferMilliseconds);
|
||||
};
|
||||
|
||||
// ── OK / Cancel ───────────────────────────────────────────────────────
|
||||
var btnOk = new Button
|
||||
{
|
||||
Text = "&OK",
|
||||
DialogResult = DialogResult.OK,
|
||||
Location = new Point(228, 600),
|
||||
Location = new Point(228, 642),
|
||||
Size = new Size(80, 27),
|
||||
};
|
||||
var btnCancel = new Button
|
||||
{
|
||||
Text = "&Cancel",
|
||||
DialogResult = DialogResult.Cancel,
|
||||
Location = new Point(316, 600),
|
||||
Location = new Point(316, 642),
|
||||
Size = new Size(80, 27),
|
||||
};
|
||||
|
||||
@@ -350,6 +368,7 @@ public sealed class AudioSettingsForm : Form
|
||||
_radioPtt, _lblPttKey, _btnChangePtt, _chkSystemWidePtt, _radioAlwaysOn,
|
||||
lblGain, _trkGain, _chkNoiseReduction, _chkStereoMic,
|
||||
_chkAux, _lblAuxDevice, _cboAuxDevice, _btnAuxRefresh, _lblAuxGain, _trkAuxGain,
|
||||
lblBuffer, _cboBuffer,
|
||||
btnOk, btnCancel,
|
||||
]);
|
||||
|
||||
@@ -534,6 +553,8 @@ public sealed class AudioSettingsForm : Form
|
||||
_settings.StereoMic = _origStereoMic;
|
||||
_settings.PttKey = (int)_origPttKey;
|
||||
_settings.SystemWidePtt = _origSystemWidePtt;
|
||||
_settings.AudioBufferMilliseconds = _origAudioBufferMilliseconds;
|
||||
_client.SetAudioBufferMilliseconds(_origAudioBufferMilliseconds);
|
||||
|
||||
if (_micStreamId != 0)
|
||||
{
|
||||
|
||||
@@ -109,8 +109,11 @@ public partial class MainForm : Form
|
||||
BootstrapFromServer();
|
||||
}
|
||||
|
||||
private void ApplyPersistedVoiceSettings() =>
|
||||
private void ApplyPersistedVoiceSettings()
|
||||
{
|
||||
_pttKey = (Keys)_voiceSettings.PttKey;
|
||||
_client.SetAudioBufferMilliseconds(_voiceSettings.AudioBufferMilliseconds);
|
||||
}
|
||||
|
||||
// ── Startup ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ public sealed class VoiceSettings
|
||||
/// to the captured PCM before feeding (the core's input gain is mic-only and global).</summary>
|
||||
public int AuxGain { get; set; } = 100;
|
||||
|
||||
/// <summary>Local capture/playback safety buffer in milliseconds: 20, 40, or 60.</summary>
|
||||
public int AudioBufferMilliseconds { get; set; } = 40;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
private static string AppDataDir => Path.Combine(
|
||||
@@ -72,7 +75,9 @@ public sealed class VoiceSettings
|
||||
{
|
||||
if (!File.Exists(FilePath)) return new VoiceSettings();
|
||||
string json = File.ReadAllText(FilePath);
|
||||
return JsonSerializer.Deserialize<VoiceSettings>(json) ?? new VoiceSettings();
|
||||
VoiceSettings result = JsonSerializer.Deserialize<VoiceSettings>(json) ?? new VoiceSettings();
|
||||
if (result.AudioBufferMilliseconds is not (20 or 40 or 60)) result.AudioBufferMilliseconds = 40;
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user