Skip to content

fix: prevent audio truncation and eliminate recording start delay - #61

Merged
jatinkrmalik merged 2 commits into
mainfrom
fix/audio-buffer-cutoff
Mar 12, 2026
Merged

fix: prevent audio truncation and eliminate recording start delay#61
jatinkrmalik merged 2 commits into
mainfrom
fix/audio-buffer-cutoff

Conversation

@jatinkrmalik

@jatinkrmalik jatinkrmalik commented Mar 12, 2026

Copy link
Copy Markdown
Member

Problems

1. Transcription cut off at the end (audio buffer truncation)

When using push-to-talk mode, releasing the hotkey after a brief pause would result in truncated transcription — the last few seconds of speech were lost.

2. ~1 second delay before mic activates

Pressing the hotkey had a noticeable lag before the microphone actually started capturing audio, making the app feel sluggish.


Root Causes

Audio buffer truncation

In AudioEngine.processAudioBuffer, audio frames were being discarded when silence detection or max-duration conditions fired. Both checks performed an early return before appending the current audio chunk to the buffer:

// BUG: max duration check returns BEFORE appending audio
if elapsed >= maxDuration {
    onMaxDurationReached?()
    return  // ← audio frame discarded
}

// BUG: silence detection returns BEFORE appending audio
if now.timeIntervalSince(lastSoundTime) >= silenceDuration {
    onSilenceDetected?()
    return  // ← audio frame discarded
}

// Audio only appended if we reach here
audioBuffer.append(...)

This meant:

  1. The audio frame that triggered the condition was always lost
  2. In push-to-talk mode, once silence duration elapsed, all subsequent audio frames with energy below the threshold were discarded (even though recording continued until key release)
  3. The silence/max-duration callbacks fired repeatedly on every audio callback, not just once

Recording start delay

AppState.startRecording() called await soundManager.playStartSoundAsync() which blocked for the entire sound duration (up to a 1-second timeout) before activating the microphone:

// BUG: blocks recording start for ~1 second
if soundEffectsEnabled {
    await soundManager.playStartSoundAsync()  // ← waits for sound to finish
}
audioEngine.startRecording(...)  // ← mic only starts after sound completes

Fixes

AudioEngine.processAudioBuffer (AudioEngine.swift)

  1. Reorder operations: Always append audio samples to the buffer before checking silence/max-duration conditions — no frames are ever discarded
  2. Add silenceCallbackFired flag: Ensures the silence detection callback fires only once per silence period (resets when speech resumes)
  3. Add maxDurationCallbackFired flag: Ensures the max duration callback fires only once per recording session
  4. Both flags are reset in startRecording() for clean state on each new session

AppState.startRecording (AppState.swift)

  • Start the microphone immediately — no more blocking on sound playback
  • Play the start sound fire-and-forget after the mic is active
  • Any brief sound bleed into the mic buffer is negligible and handled well by WhisperKit's noise model

Tests Added

6 new tests in AudioEngineTests:

  • testStopRecordingWithoutStartReturnsEmpty — baseline behavior
  • testSilenceCallbackFiresOnlyOnce — verifies the new dedup guard
  • testMaxDurationCallbackFiresOnlyOnce — verifies the new dedup guard
  • testAudioBufferNotEmptyAfterRecording — baseline recording works
  • testAudioBufferPreservedWhenSilenceDetectedkey test: buffer has data even after silence fires
  • testAudioBufferPreservedWhenMaxDurationReached — buffer has data even after max duration fires

Testing

All 57 tests pass (swift test), including the 6 new AudioEngine tests. Manually verified both fixes work correctly — recording starts instantly and full audio is captured through key release.

…detected

The AudioEngine.processAudioBuffer method had a bug where audio frames
were discarded when silence detection or max duration conditions fired.
Both checks returned early BEFORE appending the current audio chunk to
the buffer, causing the last few seconds of speech to be lost.

Changes:
- Reorder processAudioBuffer to always append audio samples to the
  buffer BEFORE checking silence/max-duration stop conditions
- Add silenceCallbackFired and maxDurationCallbackFired flags to
  prevent repeated callback invocations (previously, silence detection
  fired on every audio callback once the duration threshold was met)
- Reset both flags in startRecording() for clean state on each session

This fixes push-to-talk mode where releasing the key after a brief
pause would result in truncated transcription, because trailing audio
frames with low energy were silently dropped.
The startRecording() method awaited playStartSoundAsync() which blocked
for the entire sound duration (up to 1s timeout) before activating the
microphone. This caused a noticeable lag between pressing the hotkey and
the mic actually capturing audio.

Now the mic starts immediately and the start sound plays fire-and-forget
afterward. Any brief sound bleed into the mic buffer is negligible and
handled well by WhisperKit's noise model.
@jatinkrmalik jatinkrmalik changed the title fix: prevent audio buffer truncation when silence or max duration is detected fix: prevent audio truncation and eliminate recording start delay Mar 12, 2026
@jatinkrmalik
jatinkrmalik merged commit efd0217 into main Mar 12, 2026
1 check passed
@jatinkrmalik
jatinkrmalik deleted the fix/audio-buffer-cutoff branch August 22, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant