Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Sources/Fluid/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2475,6 +2475,11 @@ struct ContentView: View {
self.menuBarManager.setOverlayMode(.dictation)

guard !self.asr.isRunning else { return }

// Show overlay immediately so its expand animation runs in parallel
// with the engine startup, instead of waiting for isRunning to flip.
self.menuBarManager.showOverlayEarly(asrService: self.asr)

if SettingsStore.shared.enableTranscriptionSounds {
TranscriptionSoundPlayer.shared.playStartSound()
}
Expand All @@ -2499,6 +2504,8 @@ struct ContentView: View {

guard !self.asr.isRunning else { return }

self.menuBarManager.showOverlayEarly(asrService: self.asr)

// Start recording immediately for the command
DebugLogger.shared.info(
"Starting voice recording for command",
Expand Down Expand Up @@ -2537,6 +2544,8 @@ struct ContentView: View {

guard !self.asr.isRunning else { return }

self.menuBarManager.showOverlayEarly(asrService: self.asr)

// Start recording immediately for the edit instruction
DebugLogger.shared.info("Starting voice recording for edit mode", source: "ContentView")
TranscriptionSoundPlayer.shared.playStartSound()
Expand Down
20 changes: 10 additions & 10 deletions Sources/Fluid/Services/ASRService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -781,12 +781,12 @@ final class ASRService: ObservableObject {
self.engine.stop()
DebugLogger.shared.debug("✅ Engine stopped", source: "ASRService")

// Recreate the engine instance instead of calling reset() to prevent format corruption
// VoiceInk approach: tearing down and rebuilding ensures fresh, valid audio format on restart
DebugLogger.shared.debug("🗑️ Deallocating old engine and creating fresh instance...", source: "ASRService")
self.engineStorage = nil // Explicitly release old engine
// New engine will be lazily created on next access via computed property
DebugLogger.shared.debug(" Engine instance recreated", source: "ASRService")
// Keep the engine instance alive (just stopped, not deallocated) so the next
// start() skips the expensive inputNode/outputNode instantiation (~200ms).
// The old approach destroyed and recreated the engine to "prevent format corruption",
// but prepare() + start() re-negotiates the format cleanly, and startEngine() calls
// bindPreferredInputDeviceIfNeeded() to handle device changes between recordings.
DebugLogger.shared.debug("♻️ Engine stopped but kept alive for fast restart", source: "ASRService")

// CRITICAL FIX: Await completion of streaming task AND any pending transcriptions
// This prevents use-after-free crashes (EXC_BAD_ACCESS) when clearing buffer
Expand Down Expand Up @@ -964,10 +964,10 @@ final class ASRService: ObservableObject {
DebugLogger.shared.debug("✅ Engine stopped", source: "ASRService")
}

// No need to call engine.reset() here - we created a fresh engine in stop()
// Accessing the engine property will either return the existing fresh engine,
// or create a new one if this is the first start
DebugLogger.shared.debug("ℹ️ Using fresh engine instance (created lazily)", source: "ASRService")
// The engine is kept alive across recordings (just stopped, not deallocated).
// Accessing inputNode/outputNode below is cheap when the engine already exists.
// On first launch, this triggers the expensive CoreAudio node creation.
DebugLogger.shared.debug("ℹ️ Using existing engine instance (kept alive from previous session)", source: "ASRService")

// Force input node instantiation (ensures the underlying AUHAL AudioUnit exists)
DebugLogger.shared.debug("📍 Forcing input node instantiation...", source: "ASRService")
Expand Down
15 changes: 15 additions & 0 deletions Sources/Fluid/Services/MenuBarManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,21 @@ final class MenuBarManager: ObservableObject {

// MARK: - Public API for overlay management

/// Show the overlay immediately without waiting for `isRunning` to become true.
/// Called at hotkey time so the overlay expand animation runs in parallel with engine startup.
func showOverlayEarly(asrService: ASRService) {
guard !self.overlayVisible else { return }

self.pendingHideOperation?.cancel()
self.pendingHideOperation = nil
self.overlayVisible = true

NotchOverlayManager.shared.show(
audioLevelPublisher: asrService.audioLevelPublisher,
mode: self.currentOverlayMode
)
}

func updateOverlayTranscription(_ text: String) {
NotchOverlayManager.shared.updateTranscriptionText(text)
}
Expand Down
Loading