Vérifier un export WAV iOS : trames, canaux et fidélité des échantillons

Vérifier un export WAV iOS : trames, canaux et fidélité des échantillons

Un WAV exporté depuis un enregistrement peut être lisible tout en ayant perdu sa fin. Les deux canaux peuvent contenir du son, mais être inversés. Une écoute via un bureau à distance n’est pas une bonne méthode pour diagnostiquer ces problèmes. Sur un Mac cloud, commencez par faire parcourir à un signal fixe le cycle complet « écriture du fichier, fermeture, relecture, comparaison ». Branchez ensuite ces assertions sur l’exporteur iOS : les résultats seront plus faciles à reproduire.

Définir d’abord le contrat du fichier

Cet exemple vérifie uniquement une chaîne PCM en virgule flottante, sans rééchantillonnage ni modification du gain : fréquence d’échantillonnage de 48000 Hz, deux canaux et longueur de 4800 trames, soit 0.1 seconde. Le canal gauche reste à 0.25 et le canal droit à -0.5. Ces valeurs distinctes permettent de détecter une inversion, un mixage ou la duplication d’un signal mono.

En audio, une trame regroupe les échantillons de tous les canaux à un même instant. Ici, chaque canal contient donc 4800 échantillons : ce n’est pas un total de 4800 réparti sur les deux canaux. L’en-tête, les blocs supplémentaires et le format d’encodage influent sur la taille du fichier ; le nombre d’octets ne peut donc pas remplacer directement le nombre de trames.

Cette procédure valide le fichier, sans dépendre de haut-parleurs. Elle ne prouve pas le bon fonctionnement du routage audio, des périphériques Bluetooth ou de l’enregistrement sur un appareil physique. Le signal continu à niveau fixe sert uniquement aux contrôles numériques, pas à l’écoute.

Exécuter un cycle minimal sur le Mac cloud

Exécutez d’abord xcrun --find swift pour vérifier que les outils de développement actifs permettent de trouver Swift. Enregistrez le code ci-dessous dans wav-check.swift, puis lancez xcrun swift wav-check.swift depuis un répertoire accessible en écriture. Le script crée son propre répertoire temporaire et n’écrase aucun fichier existant. Si une assertion échoue, il se termine avec un code de sortie non nul.

import Foundation
import AVFoundation

let root = FileManager.default.temporaryDirectory
    .appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(
    at: root, withIntermediateDirectories: true, attributes: nil
)
defer { try? FileManager.default.removeItem(at: root) }

let url = root.appendingPathComponent("fixture.wav")
let frames: AVAudioFrameCount = 4800
let format = AVAudioFormat(
    commonFormat: .pcmFormatFloat32,
    sampleRate: 48000,
    channels: 2,
    interleaved: false
)!
let source = AVAudioPCMBuffer(
    pcmFormat: format, frameCapacity: frames
)!
source.frameLength = frames
let input = source.floatChannelData!
for i in 0..<Int(frames) {
    input[0][i] = 0.25
    input[1][i] = -0.5
}

func writeFixture() throws {
    let settings: [String: Any] = [
        AVFormatIDKey: kAudioFormatLinearPCM,
        AVSampleRateKey: 48000.0,
        AVNumberOfChannelsKey: 2,
        AVLinearPCMBitDepthKey: 32,
        AVLinearPCMIsFloatKey: true,
        AVLinearPCMIsBigEndianKey: false,
        AVLinearPCMIsNonInterleaved: false
    ]
    let writer = try AVAudioFile(
        forWriting: url,
        settings: settings,
        commonFormat: .pcmFormatFloat32,
        interleaved: false
    )
    try writer.write(from: source)
}

try writeFixture()

let reader = try AVAudioFile(
    forReading: url,
    commonFormat: .pcmFormatFloat32,
    interleaved: false
)
precondition(reader.fileFormat.sampleRate == 48000)
precondition(reader.fileFormat.channelCount == 2)
precondition(reader.length == AVAudioFramePosition(frames))

let output = AVAudioPCMBuffer(
    pcmFormat: reader.processingFormat,
    frameCapacity: frames + 1
)!
try reader.read(into: output)
precondition(output.frameLength == frames)

let channels = output.floatChannelData!
for channel in 0..<2 {
    let expected: Float = channel == 0 ? 0.25 : -0.5
    for i in 0..<Int(output.frameLength) {
        let value = channels[channel][i]
        precondition(value.isFinite)
        precondition(abs(value - expected) <= 0.000001)
    }
}
print("PASS: 4800 frames, 2 channels, samples verified")

L’objet d’écriture est placé dans une fonction pour que sa durée de vie se termine avant la relecture et que le fichier soit finalisé. Ne laissez pas un objet d’écriture encore ouvert et un objet de lecture accéder simultanément au fichier de sortie : vous risqueriez d’attribuer à tort à l’exporteur la troncature d’un fichier simplement lu trop tôt.

Le format sur disque est ici distinct du format en mémoire : le WAV stocke les données de façon entrelacée, tandis que les tampons de traitement contiennent des données flottantes non entrelacées. Pour lire les pointeurs des canaux, fiez-vous à processingFormat ; l’extension du fichier ne suffit pas à déduire l’organisation en mémoire.

Relier les contrôles à l’exporteur réel

Une fois le script minimal validé, conservez la relecture et les assertions, puis remplacez writeFixture() par l’interface d’export réellement utilisée dans le projet. Si cette interface est asynchrone, le test doit attendre le rappel de succès. Ce succès doit signifier que l’écriture du fichier est terminée, et non que la tâche vient d’être placée dans une file d’attente.

Tester les frontières entre blocs plutôt qu’une seule écriture

Couvrez au minimum des entrées de 1, 1023, 1024, 1025 et 4800 trames. Découpez ensuite le même signal en plusieurs tampons de longueurs différentes et vérifiez que le nombre de trames en sortie correspond à la somme des nombres de trames valides de chaque bloc. Le dernier bloc est généralement moins rempli que sa capacité : l’écriture doit utiliser frameLength, et non frameCapacity.

Il est également utile d’ajouter un jeu de test avec des impulsions aux deux extrémités : placez un échantillon d’amplitude différente dans la première et la dernière trame, et des zéros partout ailleurs. Cela permet de repérer un silence ajouté au début, la perte de la dernière trame ou une duplication lors de l’assemblage. Un niveau constant convient au contrôle de l’affectation des canaux ; les impulsions servent à vérifier les positions. Ces deux jeux de test ne se remplacent pas.

Valider le chemin propre à la plateforme dans une cible de test

Le script s’exécute sous macOS : il prouve seulement que le fichier de test et la logique de comparaison sont corrects. Pour couvrir le chemin d’appel réellement utilisé par l’application, placez le code d’export partagé dans une cible de test iOS et exécutez les mêmes contrôles dans le simulateur. Prévoyez ensuite des tests distincts sur appareil physique pour les parties qui dépendent de la session audio ou de l’entrée microphone.

Définir des règles d’erreur distinctes pour le PCM entier

L’écriture d’échantillons flottants en PCM entier entraîne une quantification : une assertion d’égalité stricte n’est donc plus adaptée. Fixez d’abord la plage de normalisation et la règle d’arrondi, puis définissez la tolérance en fonction du bit de poids faible. Ajoutez aussi des échantillons proches de la pleine échelle, des valeurs négatives et des zéros pour vérifier que l’écrêtage respecte le contrat.

Le rééchantillonnage relève d’un autre contrat. Le filtre peut introduire un retard, et le nombre de trames de sortie dépend aussi des arrondis et de la vidange finale des données en attente. L’assertion de cet exemple, qui exige des longueurs identiques en entrée et en sortie, ne s’y applique pas directement. Stabilisez d’abord la validation de l’export sans transformation, puis testez les conversions de format pour éviter de mêler plusieurs variables.

Recueillir les éléments de diagnostic par couche en cas d’échec

Si le nombre de trames est incorrect, vérifiez d’abord la longueur valide du dernier bloc et le moment où l’opération asynchrone est considérée comme terminée. Si le nombre de trames est correct mais que les canaux gauche et droit sont inversés, examinez leur affectation. Si toutes les valeurs sont anormales, contrôlez l’entrelacement et les conversions entre formats flottant et entier. N’augmentez pas la tolérance au premier échec : vous pourriez ainsi faire passer une erreur de format pour un résultat valide.

Pour chaque échec, consignez la séquence des longueurs des blocs d’entrée, le format du fichier, le format de traitement, le nombre réel de trames, l’erreur maximale et la position du premier échantillon incorrect. Ne conservez que de l’audio synthétique parmi les artefacts de test, afin de ne pas introduire d’enregistrements réels dans le pipeline. Les critères de validation doivent finalement reposer sur ces valeurs comparables, et non sur la seule affirmation « le fichier est lisible ».

Questions fréquentes

Peut-on vérifier un WAV sans périphérique de sortie audio ?

Oui. AVAudioFile lit et écrit les fichiers sans haut-parleur. Les trames, l’ordre des canaux et les écarts d’échantillons se vérifient hors ligne ; le routage et l’écoute nécessitent des tests distincts.

Faut-il exiger des échantillons identiques pour tous les exports WAV ?

Non. Le PCM entier introduit une quantification. Le rééchantillonnage et les changements de gain modifient aussi les valeurs. Chaque traitement doit donc avoir ses propres tolérances et règles de longueur.

Lancez votre prochain build sur une station de travail dans le cloud

Un Mac mini entier rien que pour vous, sans virtualisation. Choisissez la durée selon votre projet ; la disponibilité réelle est indiquée en temps réel dans la console.

Louer un Mac dans le cloud

Vérifiez la configuration avant de planifier votre projet

Consultez les cycles de facturation et les options de Desk M4, ou préparez votre première connexion avec le guide de démarrage.