import AVFoundation public final class AudioRecorder: AudioRecording { public enum RecorderError: Error, LocalizedError { case converterUnavailable public var errorDescription: String? { "Kon de microfoon niet omzetten naar 16 kHz mono." } } private let engine = AVAudioEngine() private let lock = NSLock() private var buffer: [Float] = [] /// RMS level (dBFS) of every delivered input buffer. Called on the audio thread; set before `start()`. public var onLevel: ((Float) -> Void)? private let targetFormat = AVAudioFormat( commonFormat: .pcmFormatFloat32, sampleRate: 16_000, channels: 1, interleaved: false )! public init() {} public func start() throws { lock.withLock { buffer.removeAll(keepingCapacity: true) } let input = engine.inputNode let inputFormat = input.outputFormat(forBus: 0) guard let converter = AVAudioConverter(from: inputFormat, to: targetFormat) else { throw RecorderError.converterUnavailable } let ratio = targetFormat.sampleRate / inputFormat.sampleRate input.removeTap(onBus: 0) input.installTap(onBus: 0, bufferSize: 1024, format: inputFormat) { [weak self] pcm, _ in guard let self else { return } if let onLevel = self.onLevel, let channel = pcm.floatChannelData?[0] { onLevel(AudioLevel.rmsDbfs(UnsafeBufferPointer(start: channel, count: Int(pcm.frameLength)))) } let capacity = AVAudioFrameCount(Double(pcm.frameLength) * ratio) + 1 guard let out = AVAudioPCMBuffer(pcmFormat: self.targetFormat, frameCapacity: capacity) else { return } var consumed = false var error: NSError? converter.convert(to: out, error: &error) { _, status in if consumed { status.pointee = .noDataNow return nil } consumed = true status.pointee = .haveData return pcm } guard error == nil, let channel = out.floatChannelData?[0] else { return } let samples = Array(UnsafeBufferPointer(start: channel, count: Int(out.frameLength))) self.lock.withLock { self.buffer.append(contentsOf: samples) } } engine.prepare() do { try engine.start() } catch { input.removeTap(onBus: 0) throw error } } public func stop() -> [Float] { engine.inputNode.removeTap(onBus: 0) engine.stop() return lock.withLock { let samples = buffer buffer = [] return samples } } }