chore: archive mac mini automation baseline
This commit is contained in:
+127
@@ -0,0 +1,127 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
enum AudioCaptureState: Equatable {
|
||||
case idle
|
||||
case starting
|
||||
case recording
|
||||
case failed(String)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class AudioCaptureManager: ObservableObject {
|
||||
@Published private(set) var state: AudioCaptureState = .idle
|
||||
|
||||
var onPCMChunk: ((Data) -> Void)?
|
||||
|
||||
private let engine = AVAudioEngine()
|
||||
private var converter: AVAudioConverter?
|
||||
private var inputFormat: AVAudioFormat?
|
||||
|
||||
private let targetFormat = AVAudioFormat(
|
||||
commonFormat: .pcmFormatInt16,
|
||||
sampleRate: 16_000,
|
||||
channels: 1,
|
||||
interleaved: true
|
||||
)!
|
||||
|
||||
func requestPermission() async -> Bool {
|
||||
await withCheckedContinuation { continuation in
|
||||
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
||||
continuation.resume(returning: granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startRecording() async {
|
||||
guard state != .recording && state != .starting else { return }
|
||||
|
||||
state = .starting
|
||||
|
||||
guard await requestPermission() else {
|
||||
state = .failed("Microphone permission was denied.")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try configureSession()
|
||||
try configureEngine()
|
||||
try engine.start()
|
||||
state = .recording
|
||||
} catch {
|
||||
stopRecording()
|
||||
state = .failed(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func stopRecording() {
|
||||
guard state == .recording || state == .starting else { return }
|
||||
|
||||
engine.inputNode.removeTap(onBus: 0)
|
||||
engine.stop()
|
||||
converter = nil
|
||||
inputFormat = nil
|
||||
state = .idle
|
||||
}
|
||||
|
||||
private func configureSession() throws {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
try session.setCategory(.playAndRecord, mode: .voiceChat, options: [.defaultToSpeaker])
|
||||
try session.setPreferredSampleRate(16_000)
|
||||
try session.setPreferredIOBufferDuration(0.02)
|
||||
try session.setActive(true)
|
||||
}
|
||||
|
||||
private func configureEngine() throws {
|
||||
let inputNode = engine.inputNode
|
||||
let hardwareFormat = inputNode.outputFormat(forBus: 0)
|
||||
inputFormat = hardwareFormat
|
||||
converter = AVAudioConverter(from: hardwareFormat, to: targetFormat)
|
||||
|
||||
inputNode.removeTap(onBus: 0)
|
||||
inputNode.installTap(onBus: 0, bufferSize: 1024, format: hardwareFormat) { [weak self] buffer, _ in
|
||||
Task { @MainActor in
|
||||
guard let self else { return }
|
||||
guard let data = self.convertToTargetPCM(buffer) else { return }
|
||||
self.onPCMChunk?(data)
|
||||
}
|
||||
}
|
||||
|
||||
engine.prepare()
|
||||
}
|
||||
|
||||
private func convertToTargetPCM(_ inputBuffer: AVAudioPCMBuffer) -> Data? {
|
||||
guard let converter else { return nil }
|
||||
|
||||
let ratio = targetFormat.sampleRate / inputBuffer.format.sampleRate
|
||||
let targetFrameCapacity = AVAudioFrameCount(Double(inputBuffer.frameLength) * ratio) + 1
|
||||
guard let outputBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: targetFrameCapacity) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var didProvideInput = false
|
||||
var conversionError: NSError?
|
||||
|
||||
converter.convert(to: outputBuffer, error: &conversionError) { _, status in
|
||||
if didProvideInput {
|
||||
status.pointee = .noDataNow
|
||||
return nil
|
||||
}
|
||||
|
||||
didProvideInput = true
|
||||
status.pointee = .haveData
|
||||
return inputBuffer
|
||||
}
|
||||
|
||||
guard conversionError == nil else { return nil }
|
||||
return outputBuffer.interleavedPCMData()
|
||||
}
|
||||
}
|
||||
|
||||
private extension AVAudioPCMBuffer {
|
||||
func interleavedPCMData() -> Data? {
|
||||
let audioBuffer = audioBufferList.pointee.mBuffers
|
||||
guard let source = audioBuffer.mData else { return nil }
|
||||
return Data(bytes: source, count: Int(audioBuffer.mDataByteSize))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user