AudioNoteRecorder.swift
100 lignes · 3619 octets
import Foundation /// Résultat d'un enregistrement terminé. struct ResultatEnregistrement { let chemin: String let dureeMsec: Int64 } /// Erreurs de AudioNoteRecorder. enum AudioNoteRecorderErreur: Error, Equatable { case enregistrementDejaEnCours } /// Enregistre un fichier WAV (PCM 16 kHz mono 16 bits) via un MoteurAudioCapture. /// /// Le moteur audio est injecté par le constructeur (MoteurAudioEngine par défaut), /// ce qui permet d'utiliser un faux en test sans accès au matériel audio. /// /// Durée maximale : 10 minutes avec arrêt automatique. /// /// API : /// - `demarrer(fichierCible:transcripteur:)` — lance l'enregistrement. /// - `arreter()` — stoppe et retourne le résultat. final class AudioNoteRecorder { private let moteur: MoteurAudioCapture private let dureeMaxSecondes: Double private var enregistrementEnCours = false private var ecriture: EcritureWav? private var fichierCourant: URL? private var transcripteurCourant: (any TranscripteurLocal)? private var debutEnregistrement: Date? private var timerArret: DispatchWorkItem? init(moteur: MoteurAudioCapture = MoteurAudioEngine(), dureeMaxSecondes: Double = 10 * 60) { self.moteur = moteur self.dureeMaxSecondes = dureeMaxSecondes } /// `true` si un enregistrement est en cours. var estEnCours: Bool { enregistrementEnCours } // MARK: - API publique /// Démarre l'enregistrement vers `fichierCible`. /// `transcripteur` est alimenté en streaming avec chaque buffer capturé. /// - Throws: `AudioNoteRecorderErreur.enregistrementDejaEnCours` si déjà actif. func demarrer(fichierCible: URL, transcripteur: (any TranscripteurLocal)? = nil) throws { guard !enregistrementEnCours else { throw AudioNoteRecorderErreur.enregistrementDejaEnCours } enregistrementEnCours = true debutEnregistrement = Date() fichierCourant = fichierCible transcripteurCourant = transcripteur ecriture = try EcritureWav(url: fichierCible) // Arrêt automatique après dureeMaxSecondes let arret = DispatchWorkItem { [weak self] in self?.arreter() } timerArret = arret DispatchQueue.main.asyncAfter(deadline: .now() + dureeMaxSecondes, execute: arret) do { try moteur.demarrer { [weak self] echantillons in self?.traiterEchantillons(echantillons) } } catch { // Rollback : le moteur n'a pas pu démarrer enregistrementEnCours = false timerArret?.cancel() timerArret = nil ecriture = nil throw error } } /// Stoppe l'enregistrement. /// - Returns: Résultat avec chemin et durée, `nil` si pas d'enregistrement actif. @discardableResult func arreter() -> ResultatEnregistrement? { guard enregistrementEnCours else { return nil } enregistrementEnCours = false timerArret?.cancel() timerArret = nil moteur.arreter() let dureeMsec = Int64((Date().timeIntervalSince(debutEnregistrement ?? Date())) * 1000) try? ecriture?.fermer() ecriture = nil guard let chemin = fichierCourant?.path else { return nil } return ResultatEnregistrement(chemin: chemin, dureeMsec: dureeMsec) } // MARK: - Traitement interne private func traiterEchantillons(_ echantillons: [Int16]) { ecriture?.ecrireEchantillons(echantillons) _ = transcripteurCourant?.accepterEchantillons(echantillons) } }
GitRust