Wenn Teams über Zeitzonen hinweg Code zur Verarbeitung von Audioaufnahmen übergeben, sind die am schwersten erkennbaren Fehler oft keine Abstürze, sondern ein kurzes Stück Ton, das am Ende fehlt. Mit derselben fest definierten Test-Fixture auf einem Cloud-Mac lässt sich das Problem schichtübergreifend reproduzieren: zuerst das Eingabeende und das vollständige Abholen der Ausgabe prüfen, dann die Framezahl. Dieser Artikel prüft ausschließlich das PCM-Resampling. Dateikodierung, Mikrofonaufnahme und Lautsprecherwiedergabe bleiben bewusst außen vor.
Zuerst den Konvertierungsvertrag festlegen
Quell- und Zielabtastrate, Kanalzahl, Interleaving, Konvertierungsqualität und primeMethod dokumentieren. Bei unterschiedlichen Abtastraten unterscheiden sich normalerweise auch die Framezahlen. „Genauso viele Frames ausgegeben wie eingegeben“ ist daher kein gültiges Erfolgskriterium.
Bei gleichbleibender Dauer ergibt sich die theoretische Zahl der Ausgabeframes aus:
Eingabeframes × Zielabtastrate ÷ Quellabtastrate
Werden beispielsweise 48000 Frames eines Monosignals mit 48000 Hz auf 44100 Hz konvertiert, sind theoretisch 44100 Ausgabeframes zu erwarten. Bei nicht ganzzahligen Ergebnissen spielen außerdem die Rundung und das Verhalten des Konverters an den Signalgrenzen eine Rolle. Die zulässige Abweichung gehört deshalb vorab in den Test, statt sie nachträglich zu vergrößern.
Ein erfolgreicher Rückgabestatus des Konverters bedeutet lediglich, dass dieser Aufruf keinen Fehler gemeldet hat. Vollständig ist die Konvertierung erst, wenn das Eingabeende ausdrücklich signalisiert und die gesamte noch im Konverter gepufferte Ausgabe abgeholt wurde.
Zunächst reduziert ein nicht verschachteltes Float32-Monosignal die Zahl der Einflussgrößen. Verwendet die Produktionspipeline andere Formate, sollten dafür separate Testfälle hinzukommen, sobald diese Baseline bestanden ist.
Die Konvertierungsschleife mit einem festen Signal prüfen
Auf einem Cloud-Mac mit installierten Kommandozeilen-Entwicklerwerkzeugen den folgenden Code als resample.swift speichern und mit swift resample.swift ausführen. Er benötigt kein Audiogerät und gibt keinen Ton wieder.
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)
Die 64 Frames sind hier eine beispielhafte Toleranz für den Test, keine Zusicherung des Frameworks. Zuerst sollte die tatsächliche Abweichung bei festgelegtem System und festgelegter Toolchain ermittelt werden. Anschließend lässt sich die Toleranz entsprechend der im Produkt erlaubten Dauerabweichung verschärfen. Bei 44100 Hz entsprechen 64 Frames etwa 1,45 Millisekunden.
Die Obergrenze der Schleife verhindert lediglich, dass die Fixture endlos läuft. Sie ist keine allgemeingültige Grenze für die Verarbeitung beliebig langer Audiosignale. Lange Aufnahmen sollten blockweise zugeführt werden; eine an die Eingabelänge angepasste Fortschrittsüberwachung muss dabei sicherstellen, dass die Verarbeitung vorankommt.
Eingabeende, Restframes und Inhalt getrennt prüfen
Der Eingabe-Callback liefert beim ersten Aufruf den Puffer und meldet danach ausdrücklich .endOfStream. .noDataNow ist nur dann richtig, wenn tatsächlich noch Daten erwartet werden, die vorübergehend nicht verfügbar sind. Wer eine Datenpause als Ende behandelt, schneidet Audio ab. Wer das Ende als Datenpause behandelt, verhindert möglicherweise den Abschluss der Konvertierung.
Auf der Ausgabeseite müssen bei jedem Aufruf zuerst die durch frameLength ausgewiesenen gültigen Daten gesammelt werden. Erst danach wird der Status ausgewertet. Gültige Frames aus demselben Aufruf dürfen nicht verworfen werden, nur weil der zurückgegebene Status bereits das Ende signalisiert. Ebenso darf nicht bis frameCapacity gelesen werden.
Fehlende Frames mit einer Endmarkierung aufspüren
Ein Sinussignal eignet sich gut, um leere Ausgaben und nicht endliche Werte zu erkennen. Ein kurzes fehlendes Endstück fällt damit jedoch weniger zuverlässig auf. Deshalb sollte eine weitere Fixture hinzukommen: zunächst Stille, im letzten Abschnitt ein Sinussignal mit niedriger Amplitude. Der Test prüft dann, ob im erwarteten Endfenster Energie vorhanden ist.
Nicht verlangen, dass das letzte Sample zwingend ungleich null ist: Der Filter verändert die Wellenform an den Signalgrenzen. Stattdessen sowohl die Ausgabelänge als auch die Energie im Endfenster speichern. So lässt sich bei einem Fehlschlag zwischen „Frames fehlen“ und „Framezahl stimmt, aber der Inhalt ist leer“ unterscheiden.
Grenzfälle abdecken, nicht nur eine Sekunde testen
Mindestens vier unabhängige Fallgruppen ergänzen: Eine reine Nulleingabe muss annähernd still bleiben; eine extrem kurze Eingabe muss regulär enden; eine Eingabelänge mit nicht ganzzahliger Umrechnung muss innerhalb der festgelegten Toleranz bleiben; bei der Konvertierung in Gegenrichtung muss die theoretische Framezahl neu berechnet werden.
Bei leerer Eingabe sollte die umgebende Anwendungslogik ausdrücklich ein leeres Ergebnis zurückgeben. Ein Puffer mit zugewiesener Kapazität, aber ohne gültige Frames darf nicht als Audiosignal ausgegeben werden. Außerdem müssen ungültige Abtastraten abgewiesen und die Länge einer einzelnen Eingabe begrenzt werden, damit die Test-Fixture nicht selbst den Arbeitsspeicher erschöpft.
Stereotests verwenden unterschiedliche Frequenzen für links und rechts und prüfen beide Ausgabekanäle getrennt. So kann eine Implementierung nicht unbemerkt bestehen, indem sie nur den linken Kanal kopiert. Nach einer Formatänderung darf die Sammellogik aus dem Beispiel, die ausschließlich den ersten Kanal liest, nicht unverändert weiterverwendet werden.
Auch beim Resampling hin und zurück ist sampleweise Gleichheit kein sinnvolles Kriterium. Filterung und Quantisierung verändern die Samples. Zu prüfen sind stattdessen Dauer, endliche Werte, Energie und zulässige Abweichungen. Der Höreindruck echter Aufnahmen wird separat abgenommen.
Bei der Übergabe über Zeitzonen hinweg belastbare Nachweise liefern
Bei jedem Regressionslauf Commit-Kennung, Betriebssystemversion, Version der Entwicklungswerkzeuge, Konvertierungsparameter, Eingabeframes, Ausgabeframes und Abweichung speichern. Bei unterschiedlichen Ergebnissen zuerst die feste Fixture erneut ausführen und anschließend die Konvertierungsstrategie vergleichen. Nicht einfach den Knoten wechseln und damit das Problem verdecken.
Die Kommandozeilen-Fixture prüft die gemeinsam genutzte Konvertierungslogik. Vor dem Merge muss dieselbe Fallgruppe dennoch im iOS-Testtarget laufen, um das Verhalten auf dem Zielsystem zu bestätigen. Die Fixture belegt weder korrekte Aufnahmeberechtigungen noch den richtigen Umgang mit Unterbrechungen der Audiositzung oder eine korrekte tatsächliche Ausgaberoute.
Das Übergabeergebnis sollte festhalten, „welche Eingabe mit welchen Parametern in welchem Zustand fehlgeschlagen ist“, und eine minimale Fixture enthalten. Damit übernimmt die nächste Schicht ausführbare Nachweise statt der bloßen Aussage: „Die Aufnahme klingt am Ende zu kurz.“
Häufig gestellte Fragen
Bedeutet noDataNow das Ende der Eingabe?
Nein. Es fehlen nur vorübergehend Daten. Ist die Quelle erschöpft, geben Sie endOfStream zurück und lesen die Ausgabe weiter, bis auch dort endOfStream gemeldet wird.
Muss die Framezahl nach dem Resampling gleich bleiben?
Nein. Die erwartete Ausgabe entspricht der Eingabeframezahl multipliziert mit dem Verhältnis von Ausgabe- zu Eingabeabtastrate. Prüfen Sie einen festgelegten Toleranzbereich bei konstantem primeMethod und gleicher Qualität.
Den passenden Cloud-Mac für Ihre nächste Aufgabe wählen
Wählen Sie einen Standort in der Nähe des Hauptstandorts Ihres Teams und nutzen Sie einen exklusiven physischen Mac mini. Für den Remote-Desktop im Browser benötigen Sie einen aktuellen Browser und eine stabile Internetverbindung. Prüfen Sie Konfiguration, Abrechnungszeitraum und Verfügbarkeit vor der Bestellung.