Skip to content
Open
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
95 changes: 95 additions & 0 deletions OpenSuperWhisper/AudioRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class AudioRecorder: NSObject, ObservableObject {

private var audioRecorder: AVAudioRecorder?
private var audioPlayer: AVAudioPlayer?
private var audioEngine: AVAudioEngine?
private var audioFile: AVAudioFile?
private var notificationSound: NSSound?
private let temporaryDirectory: URL
private var currentRecordingURL: URL?
Expand Down Expand Up @@ -126,6 +128,89 @@ class AudioRecorder: NSObject, ObservableObject {

print("start record file to \(fileURL)")

// Try using AVAudioEngine for non-interrupting recording
if let engine = setupAudioEngine(outputURL: fileURL) {
audioEngine = engine
do {
try engine.start()
isRecording = true
print("Started recording with AVAudioEngine (non-interrupting)")
} catch {
print("Failed to start audio engine: \(error)")
// Fall back to standard AVAudioRecorder
fallbackToAVAudioRecorder(fileURL: fileURL)
}
} else {
// Fall back to standard AVAudioRecorder
fallbackToAVAudioRecorder(fileURL: fileURL)
}
}

private func setupAudioEngine(outputURL: URL) -> AVAudioEngine? {
let engine = AVAudioEngine()
let inputNode = engine.inputNode

// Create the desired output format (16kHz, mono, 16-bit PCM)
let outputFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
sampleRate: 16000,
channels: 1,
interleaved: false)

guard let format = outputFormat else {
print("Failed to create output format")
return nil
}

do {
// Create audio file for writing
audioFile = try AVAudioFile(forWriting: outputURL,
settings: format.settings,
commonFormat: format.commonFormat,
interleaved: format.isInterleaved)

// Install a tap on the input node to capture audio without interrupting playback
// Use the input node's format for the tap, then convert if needed
let inputFormat = inputNode.outputFormat(forBus: 0)

inputNode.installTap(onBus: 0,
bufferSize: 1024,
format: inputFormat) { [weak self] buffer, _ in
// Convert buffer to output format if needed
if inputFormat != format {
// Create converter
guard let converter = AVAudioConverter(from: inputFormat, to: format) else {
return
}

let convertedBuffer = AVAudioPCMBuffer(pcmFormat: format,
frameCapacity: buffer.frameCapacity)

guard let outputBuffer = convertedBuffer else { return }

do {
try converter.convert(to: outputBuffer, from: buffer)
try self?.audioFile?.write(from: outputBuffer)
} catch {
print("Conversion/write error: \(error)")
}
} else {
// Direct write if formats match
do {
try self?.audioFile?.write(from: buffer)
} catch {
print("Write error: \(error)")
}
}
}

return engine
} catch {
print("Failed to setup audio engine: \(error)")
return nil
}
}

private func fallbackToAVAudioRecorder(fileURL: URL) {
let settings: [String: Any] = [
AVFormatIDKey: Int(kAudioFormatLinearPCM),
AVSampleRateKey: 16000.0,
Expand All @@ -140,13 +225,23 @@ class AudioRecorder: NSObject, ObservableObject {
audioRecorder?.delegate = self
audioRecorder?.record()
isRecording = true
print("Started recording with AVAudioRecorder (may interrupt audio)")
} catch {
print("Failed to start recording: \(error)")
currentRecordingURL = nil
}
}

func stopRecording() -> URL? {
// Stop audio engine if it's being used
if let engine = audioEngine {
engine.inputNode.removeTap(onBus: 0)
engine.stop()
audioEngine = nil
audioFile = nil
}

// Stop audio recorder if it's being used
audioRecorder?.stop()
isRecording = false

Expand Down
Loading