Moin.
Aufgrund einer PM von AndyG im englischsprachigen AutoIt-Forum habe ich mich entschlossen für mein Projekt in das deutsche Forum zu wechseln. Ich möchte einen Lautheitsmesser (offline als auch im Stream) programmieren. Grundlage sind EBU Tech 3341 für Lautheit allgemein, EBU Tech 3342 für Berechnung Lautheitsrange und ITU-R BS.1770-4 als Basis von Tech 3341.
Einer meiner ersten Ansätze war dieser hier:
- #include <Array.au3>
- #include <StringConstants.au3>
- #include <FileConstants.au3>
- Local $filename ; EBU-Test-Set: https://tech.ebu.ch/publications/ebu_loudness_test_set
- ;~ $filename = @ScriptDir & "\EBU-Test-Set\EBU-reference_listening_signal_pinknoise_500Hz_2kHz_R128.wav" ; 120 Sekunden
- $filename = @ScriptDir & "\EBU-Test-Set\1kHz Sine -20 LUFS-16bit.wav" ; 20 Sek
- ;~ $filename = @ScriptDir & "\EBU-Test-Set\1kHz Sine -26 LUFS-16bit.wav" ; 20 Sek
- ;~ $filename = @ScriptDir & "\EBU-Test-Set\1kHz Sine -40 LUFS-16bit.wav" ; 20 Sek
- ;~ $filename = @ScriptDir & "\EBU-Test-Set\seq-3341-12-24bit.wav" ; Piepen 10 Sekunden
- ; Read out file "metadata"
- Local $h = FileOpen($filename, $FO_BINARY) ; binary
- If $h = -1 Then _ErrorMessage(@error, @extended, @ScriptLineNumber - 1, "Could not open wav-file.", True, $h, "$h") ; ausführlichere Fehler-Meldung
- Local $sHeader = FileRead($h, 2000) ; 2000 Byte sollten für den Headerteil erstmal reichen
- Local $iPosition_fmt = StringInStr($sHeader, "666D7420") ; hier wird die Position von "fmt " ("666D7420" in HEX) gesucht - definierter Teil des WAV-Headers
- $iPosition_fmt = ($iPosition_fmt - 3) / 2 ; das Ergebnis wird in Position der Bytes gewandelt
- FileSetPos($h, $iPosition_fmt + 10, 0)
- Local $iChannels = Number(FileRead($h, 2)) ; fmt + 10, 2byte für Kanalanzahl
- ConsoleWrite('CH: ' & $iChannels & " ")
- Local $iSamples = Number(FileRead($h, 4)) ; direkt danach, 4byte für die Samplerate
- ConsoleWrite('SR: ' & $iSamples & " ")
- FileSetPos($h, $iPosition_fmt + 22, 0)
- Local $iBitrate = Number(FileRead($h, 2)) ; fmt + 22, 2byte für die Bitrate
- ConsoleWrite('BR: ' & $iBitrate & " ")
- FileSetPos($h, $iPosition_fmt + 28, 0)
- Local $iLengthDataBlock = Number(FileRead($h, 4)) ; fmt + 28, 4 byte aus denen später die Länge des Files berechnet werden
- Local $i100msSampleCount = $iSamples / 10 ; Anzahl der Samples für 1/10 Sekunde - 100ms-Fenster - Verschiebung des 400ms-Fensters
- Local $iNumberOfAll100msRuns = Floor($iLengthDataBlock / (($iBitrate / 8) * $i100msSampleCount * $iChannels)) ; Anzahl aller 100ms Proben bis das ganze File gescannt ist
- ConsoleWrite("L: " & $iNumberOfAll100msRuns / 10 & "s " & $filename & @CRLF)
- ; Declare further variable
- Local $iCounterOf100msRuns = 0 ; Zähler für jeden 100ms Durchgang
- Local $s100msReadOut ; 100ms ausgelesene Bytes aus dem File
- Local $a100msHexArray ; Array der 100ms Samples in Hex
- Local $iInteger ; aktuell ausgelesenes Sample
- Local $a100msInteger[0] ; Array aller Samples innerhalb eines 100ms-Fensters
- Local $a400msInteger[$i100msSampleCount * 4] ; Array aller Samples innerhalb des 400ms-Fenster für Lautheit Momentary
- Local $s100IntegerFilterKette ; String aller K-gefilterten Samples innerhalb eines 100ms-Fensters
- Local $xnK1_2, $xnK1_1, $ynK1_2, $ynK1, $ynK1_1, $xnK1 ; Variablen für K-Filter Stage 1 - x[n], x[n-1], x[n-2], y[n], y[n-1], y[n-2]
- Local $xnK2_2, $xnK2_1, $ynK2_2, $ynK2, $ynK2_1, $xnK2 ; Variablen für K-Filter Stage 2 - x[n], x[n-1], x[n-2], y[n], y[n-1], y[n-2]
- Local $iMeanSquare400ms ; Effektivwert aller 400ms-Samples
- Global $g_aAllMomentaryDecibel[$iNumberOfAll100msRuns - 3] ; die ersten drei 100ms-Fenster ergeben noch keine 400ms-Fenster zusammen, so daß noch kein Momentary-Wert berechnet werden kann
- Local $aActualMomentaryForAllChannel[0] ; enthält in Decibel den Effektivwert des aktuellen Momentary pro Kanal
- ; Start calculate all Momentary
- Local $hTimerStart = TimerInit() ; Start Zeitnahme Ermittelung sämtlicher Momentary
- ProgressOn("File-Scan - Length of file: " & $iNumberOfAll100msRuns / 10 & "s", "Please wait...", "", @DesktopWidth - 350, @DesktopHeight - 220) ; Progress während des Programmierens und Debuggings rechts unten in die Ecke um die _ArrayDisplays zu sehen
- While 1
- ProgressSet($iCounterOf100msRuns * 100 / $iNumberOfAll100msRuns, Floor($iCounterOf100msRuns * 100 / $iNumberOfAll100msRuns) & "%") ; Progress refreshen
- $s100msReadOut = FileRead($h, ($iBitrate / 8) * $i100msSampleCount * $iChannels) ; nächste 100ms des Files auslesen - $iBitrate/8 ergibt die Bytes pro Sample - mal die Anzahl der Kanäle, da die interleaved, also immer im Wechsel kommen
- If @extended < ($iBitrate / 8) * $i100msSampleCount * $iChannels Then ; wenn die Anzahl der ausgelesenen Bytes kleiner sind als für 100ms benötigt (das File ist also zu Ende)
- ExitLoop ; kein kompletter 100ms Readout mehr möglich, deshalb Daten verwerfen (laut R128)
- EndIf
- $s100msReadOut = StringTrimLeft($s100msReadOut, 2) ; "0x" entfernen um die Regex nicht zu verwirren
- $a100msHexArray = StringRegExp($s100msReadOut, "([\x00-\xff]{" & 2 * $iBitrate / 8 & "})", 3) ; sucht nach HEX-Werten 2 Zeichen pro Byte für Bitrate/8 Byte
- ;~ _ArrayDisplay($a100msHexArray,"Zeile " & @ScriptLineNumber)
- For $i = 0 To $i100msSampleCount - 1 ; für jedes Sample innerhalb 100ms
- For $j = 1 To 1;$iChannels - !!!!!!! nach Umbau von Arrays auf Variablen kann ich spontan erstmal nur einen Kanal berechnen, wenn dieser Part fertig ist, dann wird er pro Kanal dupliziert mit erweiterten Variablen
- $iInteger = $a100msHexArray[$i] ; nächster HEX-Wert
- $iInteger = _ChangeEndian($iInteger) ; aus LittleEndian BigEndian machen - AutoIt rechnet mit BigEndian-HEX
- $iInteger = Number($iInteger) ; Integer daraus machen
- $iInteger = _SignedInteger($iInteger, $iBitrate) ; Integer mit Vorzeichen daraus machen, da Audio Samples in positiven und negativen Werten in Samples gerechnet werden
- ; K-filter stage 1 - high shelving 1kHz +4dB
- $xnK1_2 = $xnK1_1 ; die Werte für x[n], x[n-1], x[n-2], y[n], y[n-1], y[n-2] stage 1 rücken eins weiter in die Vergangenheit
- $xnK1_1 = $xnK1
- $ynK1_2 = $ynK1_1
- $ynK1_1 = $ynK1
- $xnK1 = $iInteger
- $ynK1 = Int(1.53512485958697 * $xnK1 - 2.69169618940638 * $xnK1_1 + 1.19839281085285 * $xnK1_2 + 1.69065929318241 * $ynK1_1 - 0.73248077421585 * $ynK1_2) ; Koeffizienten für stage 1 hard gecoded und nicht als Variablen wegen Geschwindigkeit
- ; K-filter stage 2 - low cut 100Hz
- $xnK2_2 = $xnK2_1 ; die Werte für x[n], x[n-1], x[n-2], y[n], y[n-1], y[n-2] stage 2 rücken eins weiter in die Vergangenheit
- $xnK2_1 = $xnK2
- $ynK2_2 = $ynK2_1
- $ynK2_1 = $ynK2
- $xnK2 = $ynK1
- $ynK2 = Int($xnK2 - 2 * $xnK2_1 + $xnK2_2 + 1.99004745483398 * $ynK2_1 - 0.99007225036621 * $ynK2_2) ; Koeffizienten für stage 2 hard gecoded und nicht als Variablen wegen Geschwindigkeit
- $s100IntegerFilterKette &= $ynK2 & "|" ; gefiltertes Sample an die Kette hängen mit | als Trenner
- Next
- Next
- ; after all Samples needed for 100ms
- $s100IntegerFilterKette = StringTrimRight($s100IntegerFilterKette, 1) ; letztes | entfernen
- $a100msInteger = StringSplit($s100IntegerFilterKette, "|", $STR_NOCOUNT) ; ein Array aus der 100ms-Kette machen ohne Index
- ;~ _ArrayDisplay($a100msInteger, "IntegerKette")
- _ArrayDelete($a400msInteger, "0-" & ($i100msSampleCount - 1) & "") ; aus dem 400ms-Array (also Momentary) die obersten 100ms entfernen
- _ArrayAdd($a400msInteger, $a100msInteger) ; die aktuellen 100ms hinzufügen
- ;~ _ArrayDisplay($a400msInteger, "400ms")
- $iMeanSquare400ms = 0 ; Effektiv-Wert resetten
- If $iCounterOf100msRuns > 2 Then ; erst wenn die ersten 400ms voll sind (nach 3 Runden 100ms)
- ReDim $aActualMomentaryForAllChannel[$iChannels] ; !!! bei Surround > 5.0 wird von folgender Kanalreihenfolge ausgegangen L, R, C, LFE, Ls, Rs - Broadcast-Standard EBU
- For $i = 0 To $iChannels - 1 ; was passiert hier mit dem LFE, der nicht berechnet werden darf??????? ######################
- If $i < 3 Then ; L R C
- $aActualMomentaryForAllChannel[$i] = _Decibel(_MeanSquare($a400msInteger)) ; berechnet aus den gefilterten Samples den Effektivwert und rechnet das in Dezibel um
- ElseIf $i = 3 And $iChannels > 5 Then ; LFE
- ; nothing
- Else ; Ls Rs
- $aActualMomentaryForAllChannel[$i] = _Decibel(_MeanSquare($a400msInteger) * 1.41) ; die Surrounds werden mit 1,5dB oder Faktor 1,41 lauter bewertet
- EndIf
- Next
- ;~ _ArrayDisplay($aActualMomentaryForAllChannel)
- $iMeanSquare400ms = _AdditionDecibel($aActualMomentaryForAllChannel) ; Addition der Pegel aller Kanäle zu einem Wert
- $g_aAllMomentaryDecibel[$iCounterOf100msRuns - 3] = $iMeanSquare400ms
- EndIf
- $iCounterOf100msRuns += 1 ; Anzahl der durchgelaufenen 100ms Berechnungen um 1 erhöhen
- $s100IntegerFilterKette = "" ; Filterkette löschen
- WEnd
- FileClose($h) ; File wird nicht mehr gebraucht und geschlossen
- ProgressOff()
- ConsoleWrite("Timer: " & Round(TimerDiff($hTimerStart) / 1000) & @CRLF) ; dieses war der zeitkritische Bereich, da die meisten Berechnungen hier mit den Samples oder alle 100ms berechnet werden
- _ArraySort($g_aAllMomentaryDecibel, 1) ; damit die niedrigen Werte unten liegen
- _ArrayDisplay($g_aAllMomentaryDecibel, "All sorted")
- Local $iTresholdAbsolut = -70 ; alle Momentary unter -70dBFS sollen verworfen werden
- Local $iIndexTresholdAbsolut
- For $i = 0 To UBound($g_aAllMomentaryDecibel) - 1
- If $g_aAllMomentaryDecibel[$i] < $iTresholdAbsolut Then
- $iIndexTresholdAbsolut = $i ; ermittelt den ArrayIndex, ab dem Momentary nur noch unter -70dBFS zu finden sind
- ExitLoop
- EndIf
- Next
- Local $iLoudnessAverage ; durchschnittliche Lautheit ohne alle Werte unter -70dBFS
- Local $sRangeToDelete ; um im Array alle Werte unterhalb des Thresholds zu löschen
- If IsNumber($iIndexTresholdAbsolut) Then
- If $iIndexTresholdAbsolut = 0 Then ; alle Werte unter -70dBFS
- $iLoudnessAverage = "-INFINITE"
- Else
- $sRangeToDelete = $iIndexTresholdAbsolut & "-" & UBound($g_aAllMomentaryDecibel) - 1 ; Range ab dem Index Threshold Absolut bis zum Ende des Arrays
- ConsoleWrite("Delete Indicies Absolut: " & $sRangeToDelete & @CRLF)
- _ArrayDelete($g_aAllMomentaryDecibel, $sRangeToDelete) ; alle Werte unter -70dBFS löschen
- _ArrayDisplay($g_aAllMomentaryDecibel, "All minus -70")
- $iLoudnessAverage = _AverageDecibel($g_aAllMomentaryDecibel) ; Mittelwert der aller Momentary in Dezibel ermitteln
- $iLoudnessAverage = Round($iLoudnessAverage, 3) ; 3 Stellen nach dem Komma - bei Release nur noch 1 Stelle nach dem Komma
- EndIf
- Else ; nichts kleiner -70dBFS gefunden
- $iLoudnessAverage = _AverageDecibel($g_aAllMomentaryDecibel)
- $iLoudnessAverage = Round($iLoudnessAverage, 3)
- EndIf
- ConsoleWrite("LK Average: " & $iLoudnessAverage & @CRLF)
- Local $iLoudnessIntegrated ; Lautheit Integrated
- If IsNumber($iLoudnessAverage) Then
- Local $iTresholdRelative = $iLoudnessAverage - 10 ; der Threshold für Integrated liegt 10dB unter dem Mittelwert - alle Werte darunter werden verworfen
- Local $iIndexTresholdRelative
- For $i = 0 To UBound($g_aAllMomentaryDecibel) - 1
- If $g_aAllMomentaryDecibel[$i] < $iTresholdRelative Then ; ermittelt ArrayIndex, ab dem Momentary kleiner Relativ-Threshold
- $iIndexTresholdRelative = $i
- ExitLoop
- EndIf
- Next
- If $iIndexTresholdRelative <> "" Then ; wenn Werte unterhalb des relativen Threshold gefunden werden
- $sRangeToDelete = $iIndexTresholdRelative & "-" & UBound($g_aAllMomentaryDecibel) - 1 ; Range ab dem Index Threshold Relativ bis zum Ende des Arrays
- ConsoleWrite("Delete Indicies Relative: " & $sRangeToDelete & @CRLF)
- _ArrayDelete($g_aAllMomentaryDecibel, $sRangeToDelete) ; alle Werte unter Threshold Relativ löschen
- _ArrayDisplay($g_aAllMomentaryDecibel, "All minus relative")
- $iLoudnessIntegrated = _AverageDecibel($g_aAllMomentaryDecibel) ; Mittelwert aller restlichen Momentary in Dezibel ermitteln
- $iLoudnessIntegrated = Round($iLoudnessIntegrated, 3) ; 3 Stellen nach dem Komma - bei Release nur noch 1 Stelle nach dem Komma
- Else ; keine Werte unterhalb Threshold Relativ
- $iLoudnessIntegrated = $iLoudnessAverage
- EndIf
- Else ; LK Average = "-INFINITE", also alle Samples unter -70dBFS
- $iLoudnessIntegrated = $iLoudnessAverage
- EndIf
- ConsoleWrite("LK Integrated: " & $iLoudnessIntegrated & @CRLF)
- Exit
- #Region - Funcs
- Func _MeanSquare(ByRef $aInteger)
- Local $iNumerator ; Zähler
- For $i = 0 To UBound($aInteger) - 1
- $iNumerator += ($aInteger[$i] ^ 2) ; alle Samples erst quadrieren und dann addieren
- Next
- Local $iMeanSquare = Sqrt($iNumerator / UBound($aInteger)) ; alle quadrierten und addierten Samples durch die Anzahl Samples teilen und die Wurzel ziehen
- Return $iMeanSquare
- EndFunc ;==>_MeanSquare
- Func _AdditionDecibel($aArray) ; math by http://personal.cityu.edu.hk/~bsapplec/manipula.htm - Addiert Dezibel
- Local $iLog_1
- For $i = 0 To UBound($aArray) - 1
- $iLog_1 += (10 ^ ($aArray[$i] / 10))
- Next
- Local $iL_Addition = 10 * Log10($iLog_1)
- Return $iL_Addition
- EndFunc ;==>_AdditionDecibel
- Func _AverageDecibel($aArray) ; math by http://personal.cityu.edu.hk/~bsapplec/manipula.htm - Errechnet den Mittelwert aus Dezibel-Werten
- Local $iAnzahl = UBound($aArray)
- Local $iLog_1
- For $i = 0 To UBound($aArray) - 1
- $iLog_1 += (10 ^ ($aArray[$i] / 10))
- Next
- $iLog_1 *= (1 / $iAnzahl)
- Local $iL_Average = 10 * Log10($iLog_1)
- Return $iL_Average
- EndFunc ;==>_AverageDecibel
- Func Log10($fNb) ; Funktion für Logarithmus zur Basis 10 - AutoIt hat nur einen Logarithmus zur Basis 2: Log()
- Return Log($fNb) / Log(10)
- EndFunc ;==>Log10
- Func _Decibel($iInteger)
- ;~ Return Round(10 * Log10($iInteger / (2 ^ ($iBitrate - 1))), 3) - 0.691 ; <--- das soll der richtige Algorithmus sein laut EBU und ITU, aber der macht viel zuviel Ergebnis (die setzen ihn evtl. an anderer Stelle ein)
- Return Round(20 * Log10($iInteger / (2 ^ ($iBitrate - 1))), 3) - 0.691 ; -0.691 ist ein LKFS-Korrektor laut EBU
- EndFunc ;==>_Decibel
- Func _SignedInteger($iInteger, $iBitrate)
- If $iInteger > ((2 ^ ($iBitrate - 1)) - 1) Then ; wenn Wert größer als die Hälfte der gesamten Integer (dann sind es die negativen Samples
- $iInteger = Number($iInteger - (2 ^ $iBitrate)) ; soll die untere Hälfte ein negatives Vorzeichen bekommen - ist wird sonst automatisch Unsigned Integer angenommen
- Else
- $iInteger = Number($iInteger)
- EndIf
- Return $iInteger
- EndFunc ;==>_SignedInteger
- Func _ChangeEndian($iHex)
- Local $iChangedEndian
- For $i = 1 To StringLen($iHex) / 2
- $iChangedEndian &= StringRight($iHex, 2) ; die hinteren nach vorne setzen
- $iHex = StringTrimRight($iHex, 2) ; vom ursprünglichen gesamten LittleEndian-Hex-wert die ersten beiden Stellen löschen
- Next
- Return "0x" & $iChangedEndian
- EndFunc ;==>_ChangeEndian
- Func _ErrorMessage($iError, $iExtended, $iScrpitLineNumber, $sMessage = "", $bForcedExit = False, $vVariable = 0, $sVariableName = "")
- Local $iFlag = 262148
- Local $sOutro = "Continue?"
- If $bForcedExit Then
- $iFlag = 262144
- $sOutro = "Program exits!"
- EndIf
- Local $iMB = MsgBox($iFlag, "Error", $sMessage & @CRLF & @CRLF & 'Debug Line: ' & $iScrpitLineNumber & @CRLF & 'Error: ' & $iError & @CRLF & 'Extended: ' & $iExtended & @CRLF & 'Variable: ' & $sVariableName & @CRLF & 'Content: ' & $vVariable & @CRLF & @CRLF & $sOutro)
- If $iMB = 7 Or $bForcedExit = True Then Exit
- Return
- EndFunc ;==>_ErrorMessage
- #cs ; dieses war die ursprüngliche Funktion für K-Filter Stage 1 und 2 - da das Kopieren in eine Funktion aber relativ viel Zeit in Anspruch nimmt (und zwar für jedes einzelne Sample) wird diese Berechnung hard gecoded
- Func _K_Filter_Stage_1($xn, $xn_1, $xn_2, $yn_1, $yn_2)
- ;Global Const $g_a1K1 = -1.69065929318241
- ;Global Const $g_a2K1 = 0.73248077421585
- ;Global Const $g_b0K1 = 1.53512485958697
- ;Global Const $g_b1K1 = -2.69169618940638
- ;Global Const $g_b2K1 = 1.19839281085285
- ;Local $yn = $g_b0K1 * $xn + $g_b1K1 * $xn_1 + $g_b2K1 * $xn_2 - $g_a1K1 * $yn_1 - $g_a2K1 * $yn_2
- Local $yn = $g_b0K1 * $xn + $g_b1K1 * $xn_1 + $g_b2K1 * $xn_2 - $g_a1K1 * $yn_1 - $g_a2K1 * $yn_2
- Return Int($yn)
- EndFunc
- Func _K_Filter_Stage_2($xn, $xn_1, $xn_2, $yn_1, $yn_2)
- ;Global Const $g_a1K2 = -1.99004745483398 ; -199004745483398 14 Nullen
- ;Global Const $g_a2K2 = 0.99007225036621 ; 14 Nullen
- ;Global Const $g_b0K2 = 1.0
- ;Global Const $g_b1K2 = -2.0
- ;Global Const $g_b2K2 = 1.0
- Local $yn = $g_b0K2 * $xn + $g_b1K2 * $xn_1 + $g_b2K2 * $xn_2 - $g_a1K2 * $yn_1 - $g_a2K2 * $yn_2
- ;~ Local $yn = $xn - 2 * $xn_1 + $xn_2 + 199004745483398 * $yn_1 - 99007225036621 * $yn_2 / 1e28
- Return Int($yn)
- EndFunc
- #ce
- #EndRegion - Funcs
In diesem Code fehlt noch die Berechnung für alle Kanäle größer 1 (wegen eines Umbau des Codes erstmal weggefallen). Es wird auch noch kein 3s-Fenster berechnet, daß nötig wäre für die Lautheitsrange. Auch TruePeak ist noch nicht programmiert. Das ist alles nachrangig, denn die Berechnung für 2-kanaliges Stereo (die aufwändiges K-Filter werden nämlich schon für alle Kanäle berechnet) dauert etwas mehr als doppelte Echtzeit. Das ist natürlich nicht akzeptabel.
Hat irgendwer Ideen, wie sich die Rechenzeit runter bekommen läßt? Richtig auswirken würden sich Verbesserungen des Codes erstmal nur zwischen While1 - Wend. Denn diese Schleife berechnet alle Samples des Files.
Was ich zur Geschwindigkeitsoptimierung bereits herausgefunden und zum größten Teil beherzigt habe ist:
- Schreiben und Lesen in Variablen geht deutlich schneller als in Arrays
- Funktionsaufrufe für "kleine" aber häufige Berechnungen fressen mehr Zeit als hart im Main-Skript gecoded
Gruß, Conrad