EcritureWav.swift
100 lignes · 3711 octets
import Foundation /// Écrit un fichier WAV PCM (16 kHz mono 16 bits par défaut). /// /// Séparée d'AudioNoteRecorder pour être testable sans dépendance AVFoundation. /// L'en-tête RIFF/fmt/data est réécrit à la clôture avec les tailles exactes. /// /// Port direct de `EcritureWav.kt` (Android). final class EcritureWav { private let handle: FileHandle private let frequence: Int private var octetsData: Int64 = 0 init(url: URL, frequence: Int = 16_000) throws { self.frequence = frequence // Crée le fichier s'il n'existe pas encore if !FileManager.default.fileExists(atPath: url.path) { FileManager.default.createFile(atPath: url.path, contents: nil) } handle = try FileHandle(forWritingTo: url) // Réserve 44 octets pour l'en-tête ; sera réécrit lors de fermer() handle.write(Data(count: 44)) } /// Écrit des échantillons PCM 16 bits signés (little-endian). func ecrireEchantillons(_ data: [Int16]) { guard !data.isEmpty else { return } var bytes = Data(capacity: data.count * 2) data.forEach { sample in var le = sample.littleEndian Swift.withUnsafeBytes(of: &le) { bytes.append(contentsOf: $0) } } handle.write(bytes) octetsData += Int64(data.count * 2) } /// Finalise l'en-tête WAV et ferme le fichier. /// - Returns: Nombre d'octets de données PCM écrits. /// - Throws: `EcritureWavErreur.tailleTropGrande` si les données dépassent Int32.max. @discardableResult func fermer() throws -> Int64 { // 10 min à 16 kHz 16 bits mono ≈ 18,3 Mo — largement sous Int32.max. // La garde protège contre un enregistrement pathologiquement long ou un bug de comptage. guard octetsData <= Int64(Int32.max) else { handle.closeFile() throw EcritureWavErreur.tailleTropGrande(octetsData) } let tailleData = Int32(octetsData) // tailleFichier = octets après "RIFF"+int32 = 4(WAVE)+8(fmt tag+size)+16(fmt)+8(data tag+size)+data let tailleFichier = tailleData + 36 let canaux: Int16 = 1 let bitsParEchantillon: Int16 = 16 let byteRate = Int32(frequence) * Int32(canaux) * Int32(bitsParEchantillon) / 8 let blockAlign = canaux * (bitsParEchantillon / 8) var entete = Data(capacity: 44) entete.appendAscii("RIFF") entete.appendInt32LE(tailleFichier) entete.appendAscii("WAVE") entete.appendAscii("fmt ") entete.appendInt32LE(16) // taille du chunk fmt entete.appendInt16LE(1) // format PCM entete.appendInt16LE(canaux) entete.appendInt32LE(Int32(frequence)) entete.appendInt32LE(byteRate) entete.appendInt16LE(blockAlign) entete.appendInt16LE(bitsParEchantillon) entete.appendAscii("data") entete.appendInt32LE(tailleData) handle.seek(toFileOffset: 0) handle.write(entete) handle.closeFile() return Int64(tailleData) } } // MARK: - Erreurs enum EcritureWavErreur: Error, Equatable { case tailleTropGrande(Int64) } // MARK: - Helpers d'encodage little-endian private extension Data { mutating func appendAscii(_ string: String) { append(contentsOf: string.utf8) } mutating func appendInt16LE(_ value: Int16) { var le = value.littleEndian Swift.withUnsafeBytes(of: &le) { append(contentsOf: $0) } } mutating func appendInt32LE(_ value: Int32) { var le = value.littleEndian Swift.withUnsafeBytes(of: &le) { append(contentsOf: $0) } } }
GitRust