Compare commits

...

7 Commits

2 changed files with 38 additions and 29 deletions
-19
View File
@@ -8,7 +8,6 @@ internal sealed class AudioOutput : IDisposable
private BufferedWaveProvider? _bufferProvider; private BufferedWaveProvider? _bufferProvider;
private int _sampleRate; private int _sampleRate;
private string _deviceName = string.Empty; private string _deviceName = string.Empty;
private float _lastSample;
private bool _disposed; private bool _disposed;
public Action<string>? Log { get; set; } public Action<string>? Log { get; set; }
@@ -19,7 +18,6 @@ internal sealed class AudioOutput : IDisposable
_sampleRate = sampleRate; _sampleRate = sampleRate;
_deviceName = deviceName ?? string.Empty; _deviceName = deviceName ?? string.Empty;
_lastSample = 0f;
Stop(); Stop();
@@ -44,9 +42,6 @@ internal sealed class AudioOutput : IDisposable
ObjectDisposedException.ThrowIf(_disposed, this); ObjectDisposedException.ThrowIf(_disposed, this);
if (_bufferProvider is null) return; if (_bufferProvider is null) return;
if (samples.Length > 0)
_lastSample = samples[^1];
byte[] bytes = new byte[samples.Length * sizeof(float)]; byte[] bytes = new byte[samples.Length * sizeof(float)];
Buffer.BlockCopy(samples, 0, bytes, 0, bytes.Length); Buffer.BlockCopy(samples, 0, bytes, 0, bytes.Length);
_bufferProvider.AddSamples(bytes, 0, bytes.Length); _bufferProvider.AddSamples(bytes, 0, bytes.Length);
@@ -57,21 +52,8 @@ internal sealed class AudioOutput : IDisposable
ObjectDisposedException.ThrowIf(_disposed, this); ObjectDisposedException.ThrowIf(_disposed, this);
if (_bufferProvider is null || _sampleRate == 0) return; if (_bufferProvider is null || _sampleRate == 0) return;
// Fade out from last sample to zero over 5ms to avoid click
int fadeSamples = (int)(_sampleRate * 0.005);
float[] fade = new float[fadeSamples];
for (int i = 0; i < fadeSamples; i++)
{
float t = (float)i / fadeSamples;
fade[i] = _lastSample * (1f - t);
}
WriteSamples(fade);
// Then 500ms of silence to let NAudio drain
int padSamples = (int)(_sampleRate * 0.5); int padSamples = (int)(_sampleRate * 0.5);
WriteSamples(new float[padSamples]); WriteSamples(new float[padSamples]);
_lastSample = 0f;
} }
private void OnPlaybackStopped(object? sender, StoppedEventArgs e) private void OnPlaybackStopped(object? sender, StoppedEventArgs e)
@@ -89,7 +71,6 @@ internal sealed class AudioOutput : IDisposable
} }
_bufferProvider?.ClearBuffer(); _bufferProvider?.ClearBuffer();
_bufferProvider = null; _bufferProvider = null;
_lastSample = 0f;
} }
private static int FindDevice(string? deviceName) private static int FindDevice(string? deviceName)
+33 -5
View File
@@ -324,14 +324,46 @@ fn transcriber_loop(
// Drain trailing audio from ALSA buffer (cpal stream still alive). // Drain trailing audio from ALSA buffer (cpal stream still alive).
// Fixed 100ms window — cpal delivers every ~50ms (800 samples @ 16kHz), // Fixed 100ms window — cpal delivers every ~50ms (800 samples @ 16kHz),
// so this captures 1-2 more callbacks worth of trailing audio. // so this captures 1-2 more callbacks worth of trailing audio.
// Hold one chunk back so we can apply a fade-out to the very last one.
let drain_deadline = std::time::Instant::now() + Duration::from_millis(100); let drain_deadline = std::time::Instant::now() + Duration::from_millis(100);
let mut held_chunk: Option<Vec<f32>> = None;
while std::time::Instant::now() < drain_deadline { while std::time::Instant::now() < drain_deadline {
match rx.recv_timeout(drain_deadline - std::time::Instant::now()) { match rx.recv_timeout(drain_deadline - std::time::Instant::now()) {
Ok(chunk) => { Ok(chunk) => {
// Feed previously held chunk to Moonshine + recorder (no fade)
if let Some(prev) = held_chunk.take() {
if let Some(ref mut r) = recorder {
r.add_audio(&prev);
}
unsafe {
moonshine_transcribe_add_audio_to_stream(
handle, stream_handle,
prev.as_ptr(), prev.len() as u64,
SAMPLE_RATE, 0,
);
}
}
held_chunk = Some(chunk);
}
Err(mpsc::RecvTimeoutError::Timeout) => break,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
// Apply 5ms fade-out to the last chunk, then feed to both Moonshine and recorder
if let Some(mut chunk) = held_chunk.take() {
let fade_samples = (SAMPLE_RATE as usize * 25) / 1000; // 25ms
if chunk.len() > fade_samples {
let start = chunk.len() - fade_samples;
for i in 0..fade_samples {
let t = 1.0 - (i as f32 / fade_samples as f32);
chunk[start + i] *= t;
}
}
if let Some(ref mut r) = recorder { if let Some(ref mut r) = recorder {
r.add_audio(&chunk); r.add_audio(&chunk);
} }
unsafe { unsafe {
moonshine_transcribe_add_audio_to_stream( moonshine_transcribe_add_audio_to_stream(
handle, stream_handle, handle, stream_handle,
@@ -340,10 +372,6 @@ fn transcriber_loop(
); );
} }
} }
Err(mpsc::RecvTimeoutError::Timeout) => break,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
// If aborted (new session took over), skip final flush entirely // If aborted (new session took over), skip final flush entirely
if aborted.load(Ordering::SeqCst) { if aborted.load(Ordering::SeqCst) {