fix: remove system audio capture toggle

This commit is contained in:
Owen Gretzinger
2025-07-10 17:40:22 -04:00
parent 619223fa1e
commit 2ca102f8d6
3 changed files with 55 additions and 81 deletions
+54 -74
View File
@@ -6,11 +6,10 @@ import Foundation
import SwiftUI import SwiftUI
import ScreenCaptureKit import ScreenCaptureKit
/// Manages audio capture from microphone and/or system audio and handles real-time transcription via Deepgram /// Manages audio capture from microphone and system audio and handles real-time transcription via Deepgram
class AudioManager: NSObject, ObservableObject { class AudioManager: NSObject, ObservableObject {
@Published var transcriptChunks: [TranscriptChunk] = [] @Published var transcriptChunks: [TranscriptChunk] = []
@Published var isRecording = false @Published var isRecording = false
@Published var captureSystemAudio = true
private var audioEngine = AVAudioEngine() private var audioEngine = AVAudioEngine()
private var micSocketTask: URLSessionWebSocketTask? private var micSocketTask: URLSessionWebSocketTask?
@@ -26,15 +25,14 @@ class AudioManager: NSObject, ObservableObject {
// First ensure everything is stopped and cleaned up // First ensure everything is stopped and cleaned up
stopRecordingInternal() stopRecordingInternal()
if captureSystemAudio { // Add a small delay to ensure cleanup is complete
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// Start microphone capture in parallel with system audio // Start microphone capture in parallel with system audio
startMicrophoneTap() self.startMicrophoneTap()
// Start system audio capture asynchronously // Start system audio capture asynchronously
Task { Task {
await startSystemAudioCapture() await self.startSystemAudioCapture()
} }
} else {
startMicrophoneOnly()
} }
} }
@@ -65,85 +63,71 @@ class AudioManager: NSObject, ObservableObject {
print("Internal cleanup completed") print("Internal cleanup completed")
} }
private func startMicrophoneOnly() {
print("Starting microphone-only recording...")
// Ensure audio engine is stopped and cleaned up
cleanupAudioEngine()
// Set up audio engine for microphone capture
let inputNode = audioEngine.inputNode
let recordingFormat = inputNode.outputFormat(forBus: 0)
// Convert to the format Deepgram expects: 16kHz, 16-bit, mono
let targetFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
sampleRate: 16000,
channels: 1,
interleaved: false)!
let converter = AVAudioConverter(from: recordingFormat, to: targetFormat)!
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [weak self] (buffer, time) in
self?.processAudioBuffer(buffer, converter: converter, targetFormat: targetFormat, source: .mic)
}
audioEngine.prepare()
do {
try audioEngine.start()
DispatchQueue.main.async {
self.isRecording = true
}
connectToDeepgram(source: .mic)
print("Microphone recording started")
} catch {
print("Failed to start audio engine: \(error)")
}
}
/// Starts a microphone tap without creating a new Deepgram connection (used when also capturing system audio) /// Starts a microphone tap without creating a new Deepgram connection (used when also capturing system audio)
private func startMicrophoneTap() { private func startMicrophoneTap() {
// Ensure audio engine is stopped and cleaned up print("🎤 Starting microphone tap...")
cleanupAudioEngine()
let inputNode = audioEngine.inputNode
let recordingFormat = inputNode.outputFormat(forBus: 0)
let targetFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
sampleRate: 16000,
channels: 1,
interleaved: false)!
let converter = AVAudioConverter(from: recordingFormat, to: targetFormat)!
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [weak self] buffer, _ in
// Debug mic RMS
if let ch = buffer.floatChannelData?[0] {
let frameCount = Int(buffer.frameLength)
let samples = UnsafeBufferPointer(start: ch, count: frameCount)
let rms = sqrt(samples.map { $0 * $0 }.reduce(0, +) / Float(frameCount))
print("🎤 Mic RMS: \(rms)")
}
self?.processAudioBuffer(buffer, converter: converter, targetFormat: targetFormat, source: .mic)
}
audioEngine.prepare()
do { do {
let inputNode = audioEngine.inputNode
let recordingFormat = inputNode.outputFormat(forBus: 0)
guard let targetFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
sampleRate: 16000,
channels: 1,
interleaved: false) else {
print("❌ Failed to create target audio format for mic tap")
return
}
guard let converter = AVAudioConverter(from: recordingFormat, to: targetFormat) else {
print("❌ Failed to create audio converter for mic tap")
return
}
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [weak self] buffer, _ in
// Debug mic RMS
if let ch = buffer.floatChannelData?[0] {
let frameCount = Int(buffer.frameLength)
let samples = UnsafeBufferPointer(start: ch, count: frameCount)
let rms = sqrt(samples.map { $0 * $0 }.reduce(0, +) / Float(frameCount))
print("🎤 Mic RMS: \(rms)")
}
self?.processAudioBuffer(buffer, converter: converter, targetFormat: targetFormat, source: .mic)
}
audioEngine.prepare()
try audioEngine.start() try audioEngine.start()
connectToDeepgram(source: .mic) connectToDeepgram(source: .mic)
print("✅ Microphone tap started successfully")
} catch { } catch {
print("Failed to start microphone tap: \(error)") print("Failed to start microphone tap: \(error)")
} }
} }
private func cleanupAudioEngine() { private func cleanupAudioEngine() {
print("🧹 Cleaning up audio engine...")
// Stop the engine first
if audioEngine.isRunning { if audioEngine.isRunning {
audioEngine.stop() audioEngine.stop()
print("⏹️ Audio engine stopped")
} }
// Reset the audio engine - this safely removes all taps // Remove any existing taps on the input node
let inputNode = audioEngine.inputNode
inputNode.removeTap(onBus: 0)
print("🔇 Input tap removed")
// Reset the audio engine - this removes all connections and taps
audioEngine.reset() audioEngine.reset()
print("🔄 Audio engine reset")
// Create a fresh audio engine to ensure clean state
audioEngine = AVAudioEngine()
print("✨ Fresh audio engine created")
} }
private func startSystemAudioCapture() async { private func startSystemAudioCapture() async {
@@ -160,7 +144,6 @@ class AudioManager: NSObject, ObservableObject {
guard let display = content.displays.first else { guard let display = content.displays.first else {
print("❌ No display found") print("❌ No display found")
startMicrophoneOnly()
return return
} }
@@ -203,9 +186,6 @@ class AudioManager: NSObject, ObservableObject {
if case SCStreamError.userDeclined = error { if case SCStreamError.userDeclined = error {
print("📍 Permission denied. User needs to enable screen recording in System Settings.") print("📍 Permission denied. User needs to enable screen recording in System Settings.")
} }
// Fallback to microphone only
startMicrophoneOnly()
} }
} }
+1 -4
View File
@@ -30,10 +30,7 @@ class MeetingViewModel: ObservableObject {
.store(in: &cancellables) .store(in: &cancellables)
} }
var captureSystemAudio: Bool {
get { audioManager.captureSystemAudio }
set { audioManager.captureSystemAudio = newValue }
}
func startRecording() { func startRecording() {
audioManager.startRecording() audioManager.startRecording()
-3
View File
@@ -39,9 +39,6 @@ struct MeetingDetailView: View {
VStack(spacing: 20) { VStack(spacing: 20) {
// Recording Controls // Recording Controls
VStack(spacing: 12) { VStack(spacing: 12) {
Toggle("Capture System Audio", isOn: $viewModel.captureSystemAudio)
.disabled(viewModel.isRecording)
HStack { HStack {
Button { Button {
viewModel.startRecording() viewModel.startRecording()