Skip to content

Commit 364b18a

Browse files
chrisutheclaude
andcommitted
fix(audio): correct resampling rate adjustment and eliminate pops
Two bugs were causing resampling-based sync correction to fail: 1. The +8 sample margin in inputSamplesNeeded calculation was exactly canceling out the ~1% rate reduction. At rate 0.9914 with count=960: ceil(960 * 0.9914) + 8 = 952 + 8 = 960 = count (no reduction!) 2. Bypassing the resampler when rate ≈ 1.0 caused audible pops when transitioning in/out of resampling mode due to WDL filter state discontinuity. Fixes: - Remove +8 margin: inputSamplesNeeded = ceil(count * rate) - Always route through resampler to maintain consistent filter state - Add logger for debugging rate change subscriptions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent d95d736 commit 364b18a

File tree

2 files changed

+17
-8
lines changed

2 files changed

+17
-8
lines changed

src/SendspinClient.Services/Audio/DynamicResamplerSampleProvider.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License. See LICENSE file in the project root.
33
// </copyright>
44

5+
using Microsoft.Extensions.Logging;
56
using NAudio.Dsp;
67
using NAudio.Wave;
78
using Sendspin.SDK.Audio;
@@ -27,6 +28,7 @@ public sealed class DynamicResamplerSampleProvider : ISampleProvider
2728
private readonly ISampleProvider _source;
2829
private readonly ITimedAudioBuffer? _buffer;
2930
private readonly WdlResampler _resampler;
31+
private readonly ILogger? _logger;
3032
private readonly object _rateLock = new();
3133

3234
private double _playbackRate = 1.0;
@@ -81,12 +83,14 @@ public double PlaybackRate
8183
/// </summary>
8284
/// <param name="source">The upstream sample provider to read from.</param>
8385
/// <param name="buffer">Optional buffer to subscribe to for rate change events.</param>
84-
public DynamicResamplerSampleProvider(ISampleProvider source, ITimedAudioBuffer? buffer = null)
86+
/// <param name="logger">Optional logger for debugging.</param>
87+
public DynamicResamplerSampleProvider(ISampleProvider source, ITimedAudioBuffer? buffer = null, ILogger? logger = null)
8588
{
8689
ArgumentNullException.ThrowIfNull(source);
8790

8891
_source = source;
8992
_buffer = buffer;
93+
_logger = logger;
9094
WaveFormat = source.WaveFormat;
9195

9296
// Initialize WDL resampler
@@ -104,6 +108,11 @@ public DynamicResamplerSampleProvider(ISampleProvider source, ITimedAudioBuffer?
104108
if (_buffer != null)
105109
{
106110
_buffer.TargetPlaybackRateChanged += OnTargetPlaybackRateChanged;
111+
_logger?.LogDebug("Subscribed to TargetPlaybackRateChanged event from buffer");
112+
}
113+
else
114+
{
115+
_logger?.LogWarning("No buffer provided - rate changes will not be received!");
107116
}
108117
}
109118

@@ -121,16 +130,15 @@ public int Read(float[] buffer, int offset, int count)
121130
currentRate = _playbackRate;
122131
}
123132

124-
// If rate is 1.0 (or very close), bypass resampling for efficiency
125-
if (Math.Abs(currentRate - 1.0) < 0.0001)
126-
{
127-
return _source.Read(buffer, offset, count);
128-
}
133+
// NOTE: We intentionally do NOT bypass the resampler even at rate 1.0.
134+
// Bypassing causes audible pops when transitioning in/out of resampling mode
135+
// because the WDL resampler has internal filter state that gets disrupted.
136+
// At rate 1.0, the resampler acts as a passthrough but maintains consistent state.
129137

130138
// Calculate how many source samples we need for the requested output samples
131139
// At rate > 1.0 (speeding up), we need MORE input samples
132140
// At rate < 1.0 (slowing down), we need FEWER input samples
133-
var inputSamplesNeeded = (int)Math.Ceiling(count * currentRate) + WaveFormat.Channels * 4;
141+
var inputSamplesNeeded = (int)Math.Ceiling(count * currentRate);
134142

135143
// Ensure source buffer is large enough
136144
if (_sourceBuffer.Length < inputSamplesNeeded)
@@ -140,6 +148,7 @@ public int Read(float[] buffer, int offset, int count)
140148

141149
// Read from source
142150
var inputRead = _source.Read(_sourceBuffer, 0, inputSamplesNeeded);
151+
143152
if (inputRead == 0)
144153
{
145154
// Source is empty - fill with silence

src/SendspinClient.Services/Audio/WasapiAudioPlayer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ public void SetSampleSource(IAudioSampleSource source)
201201
// Optionally wrap with resampler for smooth sync correction
202202
if (_useResampling && buffer != null)
203203
{
204-
_resampler = new DynamicResamplerSampleProvider(_sampleProvider, buffer);
204+
_resampler = new DynamicResamplerSampleProvider(_sampleProvider, buffer, _logger);
205205
_wasapiOut.Init(_resampler);
206206
_logger.LogDebug("Sample source configured with dynamic resampling for sync correction");
207207
}

0 commit comments

Comments
 (0)