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
Alles anzeigen
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