MoteurAudioCapture.swift
94 lignes · 3504 octets
import AVFoundation /// Abstraction du moteur d'enregistrement audio. /// /// Isole AVAudioEngine pour rendre AudioNoteRecorder testable sans simulateur audio. /// L'implémentation réelle est MoteurAudioEngine ; un faux est fourni dans les tests. protocol MoteurAudioCapture: AnyObject { /// Démarre la capture. /// Le callback reçoit les échantillons PCM Int16 à 16 kHz mono déjà convertis. /// - Throws: si le moteur ne peut pas démarrer (permission refusée, matériel absent). func demarrer(onEchantillons: @escaping ([Int16]) -> Void) throws /// Arrête la capture et libère les ressources. func arreter() } // MARK: - Implémentation réelle /// Implémentation de MoteurAudioCapture basée sur AVAudioEngine. /// /// Convertit automatiquement le format natif du matériel en PCM 16 kHz mono Int16 /// via AVAudioConverter si nécessaire. final class MoteurAudioEngine: MoteurAudioCapture { private let engine = AVAudioEngine() private let formatCible: AVAudioFormat init() { formatCible = AVAudioFormat( commonFormat: .pcmFormatInt16, sampleRate: 16_000, channels: 1, interleaved: true )! } func demarrer(onEchantillons: @escaping ([Int16]) -> Void) throws { let inputNode = engine.inputNode let formatEntree = inputNode.outputFormat(forBus: 0) guard let convertisseur = AVAudioConverter(from: formatEntree, to: formatCible) else { throw MoteurAudioErreur.conversionImpossible } let tailleBuffer: AVAudioFrameCount = 1_024 inputNode.installTap(onBus: 0, bufferSize: tailleBuffer, format: formatEntree) { [weak self] buffer, _ in guard let self else { return } self.convertirEtEnvoyer(buffer: buffer, formatEntree: formatEntree, convertisseur: convertisseur, onEchantillons: onEchantillons) } try engine.start() } func arreter() { engine.inputNode.removeTap(onBus: 0) engine.stop() } // MARK: - Conversion PCM private func convertirEtEnvoyer(buffer: AVAudioPCMBuffer, formatEntree: AVAudioFormat, convertisseur: AVAudioConverter, onEchantillons: @escaping ([Int16]) -> Void) { let frameCount = AVAudioFrameCount( ceil(Double(buffer.frameLength) * formatCible.sampleRate / formatEntree.sampleRate) ) guard let sortie = AVAudioPCMBuffer(pcmFormat: formatCible, frameCapacity: frameCount) else { return } var erreurConversion: NSError? var bufferFourni = false let status = convertisseur.convert(to: sortie, error: &erreurConversion) { _, outStatus in if bufferFourni { outStatus.pointee = .noDataNow return nil } bufferFourni = true outStatus.pointee = .haveData return buffer } guard status != .error, let ptr = sortie.int16ChannelData else { return } let count = Int(sortie.frameLength) guard count > 0 else { return } let echantillons = Array(UnsafeBufferPointer(start: ptr[0], count: count)) onEchantillons(echantillons) } } // MARK: - Erreurs enum MoteurAudioErreur: Error { case conversionImpossible }
GitRust