Tester la fin et la durée d’un rééchantillonnage audio iOS

Tester la fin et la durée d’un rééchantillonnage audio iOS

Tester la fin et la durée d’un rééchantillonnage audio iOS

Lorsqu’une équipe répartie sur plusieurs fuseaux horaires transmet du code de conversion d’enregistrements audio, le défaut le plus difficile à repérer n’est souvent pas un plantage, mais un petit morceau de son manquant à la fin. Exécuter le même jeu de données de test déterministe sur un Mac dans le cloud, en vérifiant d’abord la fin de l’entrée et la vidange de la sortie, puis le nombre de trames, permet aux équipes qui se relaient de reproduire le même problème. Cet article porte uniquement sur la validation du rééchantillonnage PCM : l’encodage des fichiers, la capture au microphone et la lecture sur haut-parleur restent hors du périmètre.

Définir d’abord le contrat de conversion

Consignez la fréquence d’échantillonnage source, la fréquence cible, le nombre de canaux, le mode d’entrelacement, la qualité de conversion et primeMethod. Lorsque les fréquences diffèrent, le nombre de trames change généralement lui aussi : obtenir autant de trames en sortie qu’en entrée n’est donc pas un critère de réussite.

À durée constante, le nombre théorique de trames en sortie est :

nombre de trames en entrée × fréquence cible ÷ fréquence source

Par exemple, convertir un signal mono de 48000 trames à 48000 Hz vers 44100 Hz donne théoriquement 44100 trames en sortie. Lorsque le résultat n’est pas entier, l’arrondi et le comportement du convertisseur aux limites entrent aussi en jeu. La tolérance doit être définie dans le test, et non élargie après coup.

Un retour sans erreur du convertisseur signifie seulement que l’appel en cours n’a pas échoué. Pour garantir un résultat complet, il faut aussi signaler explicitement la fin de l’entrée et récupérer toute la sortie encore présente dans le convertisseur.

Commencez par du Float32 mono non entrelacé pour limiter les variables. Si la chaîne de production utilise d’autres formats, ajoutez des cas distincts une fois cette base validée.

Valider la boucle de conversion avec un signal déterministe

Sur un Mac dans le cloud équipé des outils de développement en ligne de commande, enregistrez le code ci-dessous dans resample.swift, puis exécutez swift resample.swift. Aucun périphérique audio n’est nécessaire et aucun son n’est lu.

import AVFoundation
import Foundation

enum CheckError: Error {
    case setup
    case conversion
    case stalled
}

func resample(_ input: AVAudioPCMBuffer, rate: Double) throws -> [Float] {
    guard
        let format = AVAudioFormat(
            commonFormat: .pcmFormatFloat32,
            sampleRate: rate,
            channels: 1,
            interleaved: false
        ),
        let converter = AVAudioConverter(from: input.format, to: format)
    else {
        throw CheckError.setup
    }

    converter.primeMethod = .none
    converter.sampleRateConverterQuality = AVAudioQuality.high.rawValue

    var supplied = false
    var samples: [Float] = []

    for _ in 0..<1000 {
        guard let output = AVAudioPCMBuffer(
            pcmFormat: format,
            frameCapacity: 4096
        ) else {
            throw CheckError.setup
        }

        var error: NSError?
        let status = converter.convert(to: output, error: &error) {
            _, inputStatus in
            if supplied {
                inputStatus.pointee = .endOfStream
                return nil
            }
            supplied = true
            inputStatus.pointee = .haveData
            return input
        }

        if let error = error {
            throw error
        }
        if status == .error {
            throw CheckError.conversion
        }

        if output.frameLength > 0 {
            guard let channel = output.floatChannelData?[0] else {
                throw CheckError.setup
            }
            samples.append(contentsOf: UnsafeBufferPointer(
                start: channel,
                count: Int(output.frameLength)
            ))
        }

        if status == .endOfStream {
            return samples
        }
    }

    throw CheckError.stalled
}

let sourceRate = 48000.0
let targetRate = 44100.0
let frames: AVAudioFrameCount = 48000

guard
    let sourceFormat = AVAudioFormat(
        commonFormat: .pcmFormatFloat32,
        sampleRate: sourceRate,
        channels: 1,
        interleaved: false
    ),
    let input = AVAudioPCMBuffer(
        pcmFormat: sourceFormat,
        frameCapacity: frames
    ),
    let channel = input.floatChannelData?[0]
else {
    throw CheckError.setup
}

input.frameLength = frames
for index in 0..<Int(frames) {
    channel[index] = Float(
        0.25 * sin(2 * Double.pi * 1000 * Double(index) / sourceRate)
    )
}

let output = try resample(input, rate: targetRate)
let expected = Double(frames) * targetRate / sourceRate
let frameError = abs(Double(output.count) - expected)

print("frames=\(output.count), frameError=\(frameError)")
precondition(!output.isEmpty)
precondition(output.allSatisfy { $0.isFinite })
precondition(frameError <= 64)

La tolérance de 64 trames est ici un critère d’acceptation donné à titre d’exemple, pas une garantie du framework. Mesurez d’abord l’écart réel avec une version fixe du système et de la chaîne d’outils, puis resserrez la tolérance selon l’erreur de durée acceptable pour le produit. À 44100 Hz, 64 trames représentent environ 1.45 milliseconde.

La limite d’itérations sert uniquement à empêcher le test de tourner indéfiniment ; ce n’est pas une limite générale pour traiter des données audio de longueur quelconque. Pour les enregistrements longs, fournissez les données par blocs et adaptez le contrôle de progression à la longueur de l’entrée.

Vérifier séparément la fin, la vidange et le contenu

Le rappel d’entrée fournit le tampon au premier appel, puis renvoie explicitement .endOfStream aux appels suivants. N’utilisez .noDataNow que lorsque les données réelles sont temporairement indisponibles. Confondre une absence temporaire avec une fin de flux tronque l’audio ; confondre une fin de flux avec une absence temporaire peut empêcher la conversion de se terminer.

Côté sortie, récupérez à chaque appel les données valides indiquées par frameLength avant de traiter l’état renvoyé. Ne jetez pas les trames valides d’un appel sous prétexte que son état indique déjà la fin du flux, et ne lisez pas jusqu’à frameCapacity.

Repérer les trames perdues avec un marqueur de fin

Une sinusoïde permet de détecter facilement une sortie vide ou des valeurs non finies, mais révèle moins bien la perte d’une courte portion finale. Ajoutez un second signal de test, silencieux au début et terminé par une sinusoïde de faible amplitude, puis vérifiez la présence d’énergie dans la fenêtre de fin attendue.

N’exigez pas que le dernier échantillon soit non nul : les filtres modifient la forme d’onde aux limites. Conservez à la fois la longueur de sortie et l’énergie de la fenêtre finale, afin de distinguer une perte de trames d’un contenu vide malgré un nombre de trames correct.

Couvrir les cas limites, pas seulement une seconde d’audio

Ajoutez au moins quatre groupes de cas indépendants : une entrée entièrement nulle doit rester pratiquement silencieuse ; une entrée très courte doit se terminer normalement ; une longueur dont la conversion ne donne pas un entier doit respecter la tolérance définie ; une conversion dans le sens inverse doit faire l’objet d’un nouveau calcul du nombre théorique de trames.

Pour une entrée vide, la couche d’encapsulation applicative doit renvoyer explicitement un résultat vide. Ne faites pas passer pour de l’audio un tampon doté d’une capacité, mais dépourvu de trames valides. Rejetez également les fréquences d’échantillonnage non valides et limitez la longueur d’une entrée unique, afin que le test lui-même n’épuise pas la mémoire.

Pour les tests stéréo, utilisez des fréquences différentes à gauche et à droite, puis vérifiez séparément les deux canaux de sortie : une implémentation qui se contente de dupliquer le canal gauche ne doit pas pouvoir passer le test. Après avoir changé de format, vous ne pouvez plus conserver la logique de collecte de l’exemple, qui ne lit que le premier canal.

Un rééchantillonnage aller-retour ne doit pas non plus être évalué par une égalité échantillon par échantillon. Le filtrage et la quantification modifient les échantillons. Vérifiez la durée, le caractère fini des valeurs, l’énergie et les tolérances admises ; l’évaluation à l’écoute des enregistrements réels doit rester distincte.

Transmettre des preuves d’un fuseau horaire à l’autre

À chaque test de non-régression, conservez l’identifiant du commit, la version du système, celle des outils de développement, les paramètres de conversion, les nombres de trames en entrée et en sortie, ainsi que l’écart mesuré. En cas de différence, relancez d’abord le jeu de données déterministe, puis comparez les stratégies de conversion. Ne changez pas directement de nœud au risque de masquer le problème.

Le test en ligne de commande valide la logique de conversion partagée. Avant la fusion, exécutez néanmoins les mêmes cas dans une cible de test iOS pour confirmer le comportement sur le système visé. Ce test ne prouve pas que les autorisations d’enregistrement, les interruptions de session audio ou le routage réel de la sortie fonctionnent correctement.

La conclusion de la passation doit préciser « quelle entrée, avec quels paramètres, a échoué dans quel état », avec un cas de test minimal en pièce jointe. L’équipe suivante reçoit ainsi des preuves exécutables, et non une simple remarque du type « l’audio semble plus court ».

Questions fréquentes

noDataNow indique-t-il que les données sont terminées ?

Non. Il signale une absence temporaire de données. À la fin réelle, renvoyez endOfStream et continuez à récupérer la sortie jusqu’à son état endOfStream.

Le nombre de trames doit-il rester identique ?

Non. Multipliez le nombre de trames entrantes par le rapport entre les fréquences de sortie et d’entrée. Appliquez une tolérance explicite, avec primeMethod et la qualité de conversion fixés.

Choisissez un Mac cloud pour votre prochain projet

Choisissez un emplacement proche de celui où se trouve principalement votre équipe, avec un Mac mini physique qui vous est entièrement dédié. Le bureau à distance dans le navigateur nécessite un navigateur récent et une connexion stable. Vérifiez la configuration, la durée de facturation et la disponibilité avant de commander.

Rapprochez votre prochain point de connexion

Vérifiez d’abord la configuration et la durée de facturation, puis choisissez un emplacement proche de la région où votre équipe travaille principalement.