FIR-Streaming für iOS prüfen: Blockzustand und Filterauslauf
Die Audiofunktion unter Windows ist fertig. Beim Wechsel auf einen Cloud-Mac für die iOS-Tests funktioniert die Filterung der gesamten Eingabe korrekt, bei blockweiser Verarbeitung treten jedoch Spitzen an den Übergängen auf. Ändern Sie zunächst weder die Lautstärke noch die Koeffizienten: Meist muss geprüft werden, ob die bisherigen Samples über Blockgrenzen hinweg erhalten bleiben und ob beim Beenden der Filterauslauf vollständig ausgegeben wird. Im Folgenden dient eine rein skalare Swift-Implementierung als Referenz, mit der sich spätere Optimierungen vergleichen lassen.
Zuerst den Ein-/Ausgabevertrag festlegen
Dieser Artikel behandelt einen kausalen FIR-Filter für einen endlichen Mono-Eingangsstrom. Dabei multipliziert h[0] das aktuelle Eingangssample und h[1] das vorherige. Die gesamte Historie vor Beginn des Streams ist mit Nullen belegt. Während der Verarbeitung entsteht für jedes Eingangssample ein Ausgangssample; nach dem Ende wird der Filterauslauf durch das Einspeisen von Nullen ausgegeben.
Für eine nicht leere Eingabe mit N Samples und M Koeffizienten muss die vollständige Ausgabe die Länge N+M−1 haben. Ein leerer Eingangsstrom liefert ein leeres Array. Wiederholtes Beenden darf keinen weiteren Auslauf erzeugen, und nach dem Beenden dürfen keine Eingaben mehr angehängt werden. Benötigt die Anwendung nur N Ausgangssamples, muss dies ausdrücklich als Vertrag mit abgeschnittener Ausgabe festgelegt werden. Die fehlenden Samples sind kein bloßes Implementierungsdetail.
Blöcke markieren nur Transportgrenzen, keine Signalgrenzen. Der Filter darf weder am Ende jedes Blocks geleert noch seine Historie zu Beginn des nächsten Blocks zurückgesetzt werden.
Verwenden Sie für die Tests ein asymmetrisches Koeffizientenarray. So fällt auch eine vertauschte Koeffizientenreihenfolge auf. Der subjektive Höreindruck einer glättenden Wirkung sollte nicht das erste Kriterium für die Korrektheit sein.
Ein minimales Testpaket auf dem Cloud-Mac anlegen
Prüfen Sie, ob eine nutzbare Entwicklungs-Toolchain ausgewählt ist, und legen Sie anschließend ein separates Verzeichnis an:
xcrun swift --version
mkdir FIRContract
cd FIRContract
swift package init --type library --name FIRContract
Speichern Sie die folgende Implementierung in Sources/FIRContract/FIRContract.swift. Sie verschiebt Arrayelemente; der Rechenaufwand wächst mit der Anzahl der Koeffizienten. Sie soll eine gut lesbare Korrektheitsreferenz liefern und eignet sich nicht für den direkten Einsatz in einem Echtzeit-Audiocallback.
struct FIR {
private let taps: [Double]
private var history: [Double]
private var hasInput = false
private var ended = false
init(taps: [Double]) {
precondition(!taps.isEmpty)
precondition(taps.allSatisfy { $0.isFinite })
self.taps = taps
self.history = Array(repeating: 0, count: taps.count)
}
mutating func process(_ input: [Double]) -> [Double] {
precondition(!ended)
return input.map { sample in
precondition(sample.isFinite)
hasInput = true
history.insert(sample, at: 0)
history.removeLast()
return zip(taps, history).reduce(0.0) {
$0 + $1.0 * $1.1
}
}
}
mutating func finish() -> [Double] {
guard !ended else { return [] }
let tail = hasInput
? process(Array(repeating: 0, count: taps.count - 1))
: []
ended = true
return tail
}
}
Sowohl Koeffizienten als auch Samples müssen endliche Werte sein. Andernfalls könnte ein einzelner ungültiger Eingangswert mehrere nachfolgende Ausgaben verfälschen. Hier drücken Vorbedingungen den programminternen Vertrag aus. Bei externen Audiodateien sollte die Validierung dagegen vor dem Aufruf einen behandelbaren Fehler zurückgeben. Beschädigte Daten dürfen nicht durch einen Prozessabbruch behandelt werden.
Koeffizientenreihenfolge und blockübergreifende Historie mit einem festen Vektor prüfen
Ersetzen Sie den Inhalt der erzeugten Testdatei durch den folgenden Code. Für die Eingabe [2,4,6] und die Koeffizienten [0.5,0.25] ergibt die Handrechnung die vollständige Ausgabe [1,2.5,4,1.5]. Das zweite Ausgangssample muss sowohl den Beitrag des aktuellen als auch den des vorherigen Eingangssamples enthalten.
import XCTest
@testable import FIRContract
final class FIRContractTests: XCTestCase {
private func render(_ chunks: [[Double]]) -> [Double] {
var filter = FIR(taps: [0.5, 0.25])
var output: [Double] = []
for chunk in chunks {
output += filter.process(chunk)
}
output += filter.finish()
XCTAssertTrue(filter.finish().isEmpty)
return output
}
private func check(_ actual: [Double], _ expected: [Double]) {
XCTAssertEqual(actual.count, expected.count)
for (a, e) in zip(actual, expected) {
XCTAssertEqual(a, e, accuracy: 1e-12)
}
}
func testKnownVectorAndChunkLayouts() {
let expected = [1.0, 2.5, 4.0, 1.5]
check(render([[2, 4, 6]]), expected)
check(render([[2], [4], [6]]), expected)
check(render([[], [2, 4], [], [6], []]), expected)
check(render([[2], [4, 6]]), expected)
}
func testImpulseAndEmptyStream() {
check(render([[1], [0, 0]]), [0.5, 0.25, 0, 0])
check(render([[], []]), [])
}
func testSingleTapHasNoTail() {
var filter = FIR(taps: [2])
check(filter.process([1, -1]), [2, -2])
XCTAssertTrue(filter.finish().isEmpty)
}
}
Führen Sie im Stammverzeichnis des Pakets swift test aus. Prüfen Sie zuerst den bekannten Vektor und danach die Gleichwertigkeit verschiedener Blockaufteilungen. Wer nur die Ergebnisse zweier Aufteilungen vergleicht, kann einen Fehler übersehen, den beide Verarbeitungspfade gemeinsam haben. Ein Einheitsimpuls macht Fehler in der Koeffizientenreihenfolge und im Anfangszustand der Historie unmittelbar sichtbar.
Blockgrenzen als Regressionstestfälle erfassen
Alle Trennstellen durchgehen, statt auf das nächste Störgeräusch zu warten
Prüfen Sie bei einer kurzen, festen Eingabe jede mögliche Position für eine einzelne Trennstelle, einschließlich Anfang und Ende. Legen Sie für jede Aufteilung einen neuen Filter an und vergleichen Sie das Ergebnis mit der Verarbeitung der gesamten Eingabe. Ergänzen Sie Aufteilungen mit jeweils einem Sample pro Block, wechselnden Blockgrößen und Blocklängen unterhalb der Koeffizientenanzahl. Prüfen Sie insbesondere, ob der Auslauf auch bei M größer als N vollständig bleibt.
Die Eingaben sollten außerdem reine Nullfolgen, abwechselnd positive und negative Werte, konstante Werte und Impulse abdecken. Prüfen Sie die Ausgabelänge zuerst mit einer eigenen Assertion. Ein reiner Wertevergleich mit zip reicht nicht aus, da er den verbleibenden Teil des längeren Arrays ignoriert.
Das Streamende von vorübergehend fehlenden Daten unterscheiden
Ein leerer Block bedeutet lediglich, dass diesmal keine Samples vorliegen, nicht dass der Stream beendet ist. Rufen Sie finish() erst auf, wenn die vorgeschaltete Quelle das Ende bestätigt hat. Wird die Eingabe nach einer kurzen Pause fortgesetzt, muss die Historie erhalten bleiben. Legen Sie beim Test einer erneuten Wiedergabe eine neue Instanz an und prüfen Sie, dass der neue Stream keinen Auslauf des vorherigen Streams enthält.
Bei Mehrkanalaudio muss jeder Kanal eine eigene Historie führen. Werden ineinander verschachtelte Samples des linken und rechten Kanals direkt an den Monofilter übergeben, behandelt er sie wie zeitlich aufeinanderfolgende Samples desselben Kanals. Das führt zu Übersprechen und lässt sich nicht durch eine Anpassung der Fehlertoleranz beheben.
Abnahmecheckliste vor der Integration einer optimierten Implementierung
Behalten Sie diese skalare Referenz bei, wenn Sie die Produktionsimplementierung auf einen Ringpuffer oder vektorisierte Berechnungen umstellen. Vergleichen Sie bei identischer Eingabe, identischen Koeffizienten und gleicher Abschlussstrategie sowohl die Ausgabelänge als auch den Fehler an jedem Sample. Ändert sich die Reihenfolge der Gleitkommaaddition, ist keine bitweise Übereinstimmung erforderlich. Die Regeln für absolute und relative Fehler müssen jedoch vorab feststehen und dürfen nicht nach einem fehlgeschlagenen Test nach Belieben gelockert werden.
Prüfen Sie im Echtzeitcallback zusätzlich dynamische Speicherallokationen, Sperren und das verfügbare Verarbeitungsbudget. Die Tests in diesem Artikel belegen weder Echtzeittauglichkeit noch, dass der Frequenzgang des Filters die Produktanforderungen erfüllt. Auch nach erfolgreichen Tests auf dem Host müssen die Tests im iOS-Test-Target erneut laufen. Überprüfen Sie außerdem auf einem physischen Gerät das Eingabeformat, die Kanalzuordnung und das Verhalten bei kontinuierlicher Wiedergabe.
Archivieren Sie mit den Tests die Commit-Kennung, die Toolchain-Version, die Koeffizienten, die Blockfolge und die Fehlerposition. Nur wenn sich jede Auffälligkeit an einem Blockübergang auf eine feste Eingabe zurückführen lässt, können Zustandsfehler im Algorithmus, Fehler bei der Formatkonvertierung und Probleme in der tatsächlichen Wiedergabekette voneinander unterschieden werden.
Häufig gestellte Fragen
Warum entstehen Übergangsfehler bei unabhängiger Blockfilterung?
Ein FIR-Ausgabewert hängt von früheren Eingabewerten ab. Eine Neuinitialisierung je Block verwirft diese Historie und setzt vor jede Grenze implizit Nullen. Jeder Stream braucht einen durchgehenden eigenen Zustand.
Wie viele Ausgabewerte muss ein abgeschlossener Stream liefern?
Für N Eingabewerte mit N größer null und M Filterkoeffizienten liefert die vollständige lineare Faltung N+M−1 Werte. Gibt eine Schnittstelle nur N Werte aus, muss das Abschneiden des Filterauslaufs dokumentiert sein.
Miete einen exklusiven Mac für deinen Build.
Wähle einen nahegelegenen Standort und prüfe anschließend Arbeitsspeicher, Speicherplatz und Abrechnungszeitraum für dein Projekt. Du erhältst einen vollständigen physischen Rechner exklusiv, keine virtuelle Maschine. Den aktuellen Verfügbarkeitsstatus siehst du in Echtzeit in der Konsole.