R128 Lautheitsmesser

  • 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:


    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

    SciTE4AutoIt = 3.7.3.0 AutoIt = 3.3.14.2 AutoItX64 = 0 OS = Win7Pro SP1 OSArch = X64 Language = 0407/german

    H:\...\AutoIt3\SciTE H:\...\AutoIt3 H:\...\AutoIt3\Include (H:\ = Network Drive)


    88x31.png Any of my own code posted anywhere on the forum is available for use by others without any restriction of any kind.

    Dieser Beitrag wurde bereits 1 Mal editiert, zuletzt von Simpel ()

  • Hi,

    zunächst würde ich, um all dieses Umgerechne/Hex/Array-Gedöns zu vermeiden, die WAV-Datei komplett in eine DllStruct einlesen.


    Die Daten stehen somit im Speicher und können in Autoit per DllstructGetData() an ihrer jeweiligen Position ausgelesen werden.

    Die eigentliche Berechnung innerhalb der beiden FOR/TO-Schleifen würde ich dann auslagern in entweder eine mit einer Compilersprache erstellten Dll oder direkt in Assemblercode.

    Beides reduziert die Laufzeit innerhalb der Schleifen auf nahezu NULL Millisekunden. Gehe von einer Beschleunigung der Berechnung mit Faktor 1000 aus...


    Bin zzt. unterwegs, aber in der kommenden Woche sollte ich ein erstes Script zusammengebastelt bekommen

  • Moin Andy,


    da bin ich ja sehr gespannt drauf. Die Wav-Datei komplett einzulesen hatte ich auch überlegt, aber wenn das Ding mal ne Stunde oder mehr lang ist plus max. 6-8 Kanäle hat habe ich Sorge, dass das den Speicher überfüllt.


    Aber wie gesagt, ich freue mich auf Deine Ideen und bin gespannt, wie fix das werden kann.

    Gruß, Conrad

    SciTE4AutoIt = 3.7.3.0 AutoIt = 3.3.14.2 AutoItX64 = 0 OS = Win7Pro SP1 OSArch = X64 Language = 0407/german

    H:\...\AutoIt3\SciTE H:\...\AutoIt3 H:\...\AutoIt3\Include (H:\ = Network Drive)


    88x31.png Any of my own code posted anywhere on the forum is available for use by others without any restriction of any kind.

  • Bin schon weitergekommen^^

    Für das Ein-Kanal-Beispiel aus deinem Script habe ich heute Abend schon eine (schnelle(re)) Variante.


    Generell sollte man aber überlegen, inwieweit eine "universale" Berechnung Sinn macht.

  • Moin.


    Ich habe über's Wochenende die verschiedenen Kanäle hinzufügen können:

    Es gibt noch etwas, was man vielleicht generell mit bedenken sollte. Für die Berechnung der Lautheitsrange braucht man aus den 100ms-Momentary-Werten auch ein 3s-Fenster (und nicht nur das 400ms-Fenster für Integrated). Die Daten dürfen also nicht wieder zu früh entlassen werden, sondern auch das 3s-Fenster füllen.


    Was meinst Du mit der universalen Berechnung?

    Gruß, Conrad

    SciTE4AutoIt = 3.7.3.0 AutoIt = 3.3.14.2 AutoItX64 = 0 OS = Win7Pro SP1 OSArch = X64 Language = 0407/german

    H:\...\AutoIt3\SciTE H:\...\AutoIt3 H:\...\AutoIt3\Include (H:\ = Network Drive)


    88x31.png Any of my own code posted anywhere on the forum is available for use by others without any restriction of any kind.

  • Wow,


    da bin ich mit diesem doch noch halbwegs übersichtlichen Code über die 20.000 Zeichen Beschränkung gekommen? Habe ordentlich was löschen müssen (Kommentarzeilen und Leerzeilen für die Übersichtlichkeit). In Word werden die Zeichen wohl anders gezählt, denn da war ich mit dem Original-Code knapp unter 20.000 - hier wurde es aber nicht akzeptiert.


    Gruß, Conrad

    SciTE4AutoIt = 3.7.3.0 AutoIt = 3.3.14.2 AutoItX64 = 0 OS = Win7Pro SP1 OSArch = X64 Language = 0407/german

    H:\...\AutoIt3\SciTE H:\...\AutoIt3 H:\...\AutoIt3\Include (H:\ = Network Drive)


    88x31.png Any of my own code posted anywhere on the forum is available for use by others without any restriction of any kind.

  • Ach ja, das könnte Sinn ergeben.

    SciTE4AutoIt = 3.7.3.0 AutoIt = 3.3.14.2 AutoItX64 = 0 OS = Win7Pro SP1 OSArch = X64 Language = 0407/german

    H:\...\AutoIt3\SciTE H:\...\AutoIt3 H:\...\AutoIt3\Include (H:\ = Network Drive)


    88x31.png Any of my own code posted anywhere on the forum is available for use by others without any restriction of any kind.

  • Sodele, komme gerade erst dazu, bissl zu programmieren.

    Man müsste mal testen, inwieweit der "float" Datentyp für die Berechnung ausreicht. 8 Stellen Genauigkeit sollten ggf hinhauen. Dann könnte man die Berechnung in den SSE-Registern abhandeln, der gesamte Block

    wäre dann in nicht mal 10 Prozessortakten abgewatscht, und nur in einer Handvoll Zeilen ASM-Code. Auf neueren Prozessoren könnte man sogar ein FMA (fused multiply add) verwenden, also a=b+c*d und das mit 4 Registerinhalten gleichzeitig, d.h. die gesamte Zeile

    Code
    1. (1.53512485958697 * $xnK1 - 2.69169618940638 * $xnK1_1 + 1.19839281085285 * $xnK1_2 + 1.69065929318241 * $ynK1_1 - 0.73248077421585 * $ynK1_2)

    wäre mit einem (!) Prozessorbefehl abgehandelt....schaumamal



    Sollte die Genauigkeit höher sein müssen, dann muss man double verwenden, da kann man immer noch die SSE-Register verwenden. Leider passen da dort in die 128 Bit nur 2xdouble (64Bit) Zahlen rein, aber besser wie nix. Immer noch Welten schneller als die gängigen Compiler mit FPU-Code :o)

  • Also theoretisch könnten die Audiosamples signed Integer 32bit sein. Ich durchdenke aber auch gerade, ob man allen Audiosamples größer 16bit einfach die lower Bytes klaut und 16bit draus macht. Das könnte eine Ungenauigkeit in der Größe des letzten 16bit-Bits sein, vermute ich, aber es ist, glaube ich, genau genug. Die Lautheitswerte werden am Ende mit einer Dezimalstelle angegeben. Ich muss das mal konkret durchrechnen.

    Ob 8 Nachkommastellen für die Filterberechnung reicht, muß ich auch ausprobieren.


    Gruß, Conrad

    SciTE4AutoIt = 3.7.3.0 AutoIt = 3.3.14.2 AutoItX64 = 0 OS = Win7Pro SP1 OSArch = X64 Language = 0407/german

    H:\...\AutoIt3\SciTE H:\...\AutoIt3 H:\...\AutoIt3\Include (H:\ = Network Drive)


    88x31.png Any of my own code posted anywhere on the forum is available for use by others without any restriction of any kind.

  • Hi!

    Also theoretisch könnten die Audiosamples signed Integer 32bit sein. Ich durchdenke aber auch gerade, ob man allen Audiosamples größer 16bit einfach die lower Bytes klaut und 16bit draus macht. Das könnte eine Ungenauigkeit in der Größe des letzten 16bit-Bits sein, vermute ich, aber es ist, glaube ich, genau genug. Die Lautheitswerte werden am Ende mit einer Dezimalstelle angegeben. Ich muss das mal konkret durchrechnen.

    Ob 8 Nachkommastellen für die Filterberechnung reicht, muß ich auch ausprobieren.

    Daran bin ich in den letzten Stunden gescheitert...

    Nachdem ich die Berechnungen nach Assembler (die SSE-Register beinhalten 4xfloat und werden parallel in einem Takt(!) berechnet) überführt hatte, und für die ersten (hunderte bis tausende) Schleifendurchläufe exakt auf die gleichen (Integer)Werte kam, musste ich dennoch feststellen, dass es mitten in der Berechnung zu einer immer größeren "Drift" der Ergebnisse kam. Letztendlich liegt es an der Konstanten 1.99004745483398, für die es KEINE Entsprechung als float gibt.

    Für die Berechnung würde benötigt ein Wert von float 1.9900474 (man beachte die letzte Stelle hinter dem Komma, also die 4), aber DEN GIBT ES NICHT!

    Wen das näher interessiert, der sei hierhin verwiesen!

    Das letzte Bit in der Mantisse wechselt zwischen 1.9900473 und 1.9900475, und exakt dieses eine (fehlende "halbe" :Face:) Bit führt zu unterschiedlichen Ergebnissen, da sich innerhalb der Schleife der "Fehler" in den hunderten Multiplikationen immer weiter fortsetzt.

    Float hat eine Genauigkeit von 8 Stellen incl. Vorkomma, d.h. bei vier Vorkommastellen bleiben nur 4 Nachkommastellen für die Genauigkeit übrig und da summieren sich dann bei Multiplikationen mit großen Zahlen die "Fehler" gegenüber AutoIt, welches intern mit double, also 64Bit Genauigkeit rechnet.

    Man könnte natürlich in Assembler auch mit double rechnen und die seit 40 Jahren unveränderte FPU im Prozessor bemühen (genau wie das AutoIt bzw. die davon aufgerufenen Funktionen des M$-C++-Compilers machen), das wäre geschätzt Faktor 20 langsamer als der entsprechende SSE-Code für double, aber das war ja nicht die Intention. Nur mal für die Galerie, den Abschnitt


    Code
    1. $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
    2. $xnK1_1 = $xnK1
    3. $ynK1_2 = $ynK1_1
    4. $ynK1_1 = $ynK1
    5. $xnK1 = $iInteger

    wickelt der Assemblercode in 3 Prozessortakten ab (4x floatwert in einem Register einen Platz nach links shiften und den (vorher in float umgewandelten) Integer einfügen) .

    Die Zeile

    $ynK1 = Int(1.53512485958697 * $xnK1 - 2.69169618940638 * $xnK1_1 + 1.19839281085285 * $xnK1_2 + 1.69065929318241 * $ynK1_1 - 0.73248077421585 * $ynK1_2)

    wird in 12 Takten abgewickelt, davon entfallen allein 8 Takte auf die Umwandlung von float nach int und wieder nach float. Prozessor- bzw. Registerintern wird nur mit float gerechnet!



    Der Gesamtfehler im Vergleich double zu float bewegt sich bei ca. 1-2%. Leider gibt es auch seltene Ausreißer, bei denen es einen Fehler von 25% gibt. Ob du damit leben kannst, musst du herausfinden.

    Den Faktor 1000 als Geschwindigkeitsvorteil (für die Berechnungen innerhalb der Schleife) habe ich nicht ganz erreicht, aber Faktor 600 ist auch nicht schlecht^^

    Im 64-Bit-Modus hätte ich YMM-Register (256Bit) verwenden können, das wäre etwas aufwendiger geworden, hätte aber die Berechnungen massiv beschleunigt, weil KEIN(!) Speicherzugriff zum Abspeichern der Zwischenergebnisse nötig gewesen wäre!



    Generell ist dein Ansatz aus der Sicht eines AutoIt-Programmierers (ich sag es mal vorsichtig, nicht falsch verstehen!) "grenzwertig".

    Einen String aus Zahlen zu erstellen, aus dem dann ein Array gemacht wird, welches wiederum "beschnitten" und ergänzt und letztendlich noch sortiert wird, ist extrem suboptimal.

    Aber dafür kannst du eigentlich wenig, die Beschränkung liegt eher in der generell langsamen (Berechnungs-)Geschwindigkeit von AutoIt aber hauptsächlich in der Behandlung von Arrays.

    Die Daten in der Audio-Datei liegen doch sowieso schon als "Array" vor, ich habe diese einfach ausgelesen und per BinaryToString() in eine Struct aus 16-Bit-integer geschrieben. Keine weitere "Umrechnung" nötig!

    Schreibst du das Script statt "Arrays" auf eine DllStruct() um, dann sparst du dir sämtliche Umrechnungen aus String nach Array, das ArrayDelete/Add (wird ersetzt durch ein ultraschnelles Memcopy).


    Für einen Compiler mit einem 16-Bit-Array im Sourcecode wäre das ein gefundenes Fressen. Das komplette Script (und nicht nur die einzelne (Schleifenberechnung!) wäre in einigen Millisekunden abgehandelt, also ECHTZEIT!

    Und da wäre seitens Optimierung noch reichlich Luft nach oben!

    Versuch mal, dein Script einem Basic-Compiler vorzusetzen, ggf reicht es schon, eine DLL nur mit den Berechnungen/Funktionen zu erzeugen und diese aus AutoIt aufzurufen.


    Anbei der Vergleich des jeweils letzten in der Schleife berechneten Integers, also $ynk2.

    Ich habe mir geschenkt, die vom ASM-Code berechneten Werte in einen String zu schreiben (die gehören in eine 16-Bit-"short" Struct, welche ein "Array" ist!) , das hätte den Code nochmal überdimensional aufgepumpt. Nur um einen String zu machen, der dann wieder per StringSplit() in ein Array zerlegt wird uswusf....DAS war aber schon mal Thema^^ //

    //EDIT habe ich jetzt doch gemacht...


    Aber falls du die Ergebnisse trotzdem vergleichen willst, hier mal ein Ansatz....^^

    Man beachte nach dem ASM-Code die Schleife, die aus der Integer-DllStruct einen String macht, welcher dann in ein Array überführt wird:Face:Ich glaub, ich schreib das Script mal komplett auf Dllstruct um....:rock:

    //EDIT Das bereitet mir körperliche Schmerzen, ich schreibe einen ASM-Code, der aus den Integer einen String macht...


    simpel lautheit ASM_forum1.zip



    In der Anzeige der Zeit für den ASM-Code ist auch die Umwandlung in einen String und das in AutoIt SEHR langsame Lesen dieses Strings aus dem Speicher enthalten, einfach die beiden Zeilen auskommentieren....bei mir ergibt sich für die eigentliche Berechnung 0.08ms, inclusive der Umwandlung in String und das Lesen sind das 0,23ms...die Differenz von 0.11ms ist zwar nicht die Welt, aber völligst unnötig!

    Das darauffolgende Neuberechnen der Arrayinhalte dauert pro Durchlauf bei mir ca. 80-100ms.



    Ich schreibe das Script mal weiter um auf komplette Verwendung von DllStructs statt Array, das macht es selbst in reinem AutoIt-Code schätzungsweise 10x schneller!

    Aber wie gesagt, das vorliegende Script in einer Compilersprache wäre sowieso schon 100x schneller....

  • Moin Andy,


    Danke schon mal für die bisherigen Ausführungen. Ich bin in DLLStructs leider gar nicht zu Hause. Da muß ich wohl noch mal ordentlich büffeln, um das wirklich zu verstehen. Den riesigen Zeitgewinn zwischen beiden Methoden habe ich gesehen. Dabei hat sich gezeigt, daß ein weiterer großer Zeitfresser das 400ms-Array ist. Allein:

    AutoIt
    1. _ArrayDelete($a400msInteger, "0-" & ($i100msSampleCount - 1) & "") ; aus dem 400ms-Array (also Momentary) die obersten 100ms entfernen
    2. _ArrayAdd($a400msInteger, $a100msInteger) ; die aktuellen 100ms hinzufügen

    frisst pro 100ms-Runde ca. 100ms Zeit. Das ist natürlich Murks.

    Was ich aber auch sehe, wenn ich

    AutoIt
    1. _ArrayDisplay($a100msInteger, "IntegerKette")

    einschalte, daß mit reinem AutoIt das Array immer die exakten 4800 Werte hat, aber über die DLLStruct die Werte zwischen 4800 und 4812 schwanken. Weißt Du, warum das so ist?

    Was Du bisher immer berechnest, ist 1 Kanal Audio hintereinander geschrieben. Es können ja aber bis zu 6 Kanäle sein, die interleaved, im Wechsel codiert sind. In Post 5 habe ich dieses Verhalten bereits berücksichtigt. Wie bekommt man das jetzt mit Deiner Variante hin? Vergrößert sich die Struct mal der Kanäle und greift die K-Filter-Berechnung nur auf die Werte zurück, die nur zu diesem Kanal gehören?

    Ich denke mal, daß ich auch ein DLLStruct sowohl für die 400ms, als auch für die 3s schaffen muß und zwar für jeden Kanal extra. Da es Kanäle gibt, die mit Faktor 1,5 und einen Kanal mit Faktor 0 in die Gesamtberechnung eingehen.


    Gruß, Conrad

    SciTE4AutoIt = 3.7.3.0 AutoIt = 3.3.14.2 AutoItX64 = 0 OS = Win7Pro SP1 OSArch = X64 Language = 0407/german

    H:\...\AutoIt3\SciTE H:\...\AutoIt3 H:\...\AutoIt3\Include (H:\ = Network Drive)


    88x31.png Any of my own code posted anywhere on the forum is available for use by others without any restriction of any kind.

  • Es können ja aber bis zu 6 Kanäle sein, die interleaved, im Wechsel codiert sind. In Post 5 habe ich dieses Verhalten bereits berücksichtigt. Wie bekommt man das jetzt mit Deiner Variante hin? Vergrößert sich die Struct mal der Kanäle und greift die K-Filter-Berechnung nur auf die Werte zurück, die nur zu diesem Kanal gehören?

    Es kommt darauf an, wie die Kanäle innerhalb der Datei bzw. des Dateiformats vorliegen. Rudimentär hatte ich diverse "Umformungen" für WAV-Dateien mit Stereo, also 2 Kanälen, mal in Echtzeit vor fast 15 Jahren hingebastelt. Frag mich heute nicht danach^^

    Grundsätzlich stellt sich die Frage, was du mit dem Programm überhaupt beabsichtigst. Was soll das werden?

    Dann zieht man die Sache entsprechend auf.


    Mit den Structs ist das gar keine große Sache. Da werden einfach Bereiche im Speicher reserviert, und entsprechend bearbeitet. Sieht aus wie ein Array, und wird auch ähnlich angesprochen.

    Grundsätzlich kannst du mit AutoIt bei deinem Vorhaben und den Mengen an Berechnungen keinen Blumenpott gewinnen, auch nicht bei Verwendung von Structs.

    Was geht ist ein "schickes" GUI, die eigentlich einfachen Berechnungen würde ich in Funktionen in eine mit einem Basic-Compiler erstellte DLL auslagern und dann aus AutoIt callen. Dann hast du die Oberfläche in AutoIt, und die Geschwindigkeit eines Compilers.

    Mit dem großen Vorteil, dass jedermann mit egal welchem Compiler und Programmiersprache eine ggf. "schnellere" Berechnung schreiben kann.

    Da macht dann die Verwendung von ASM nur noch Sinn, wenn es wirklich um die absolute Optimierung geht. Da würde ich allerdings die Challenge gegen herkömmliche Compiler aufnehmen.:saint:

    Weiterhin sind die zu berechnenden Teile der Daten doch eher übersichtlich "klein", also eigentlich "winzig", nur einige Kilobytes.

    Eine weitere Alternative wäre dann die parallele Berechnung mehrerer Kanäle bzw. mehrerer Teilbereiche gleichzeitig. Wenn rudimentäres C (ohne ++) kein Problem darstellt, wäre auch OpenCL eine Alternative....


    daß mit reinem AutoIt das Array immer die exakten 4800 Werte hat, aber über die DLLStruct die Werte zwischen 4800 und 4812 schwanken. Weißt Du, warum das so ist?

    Nein, die Struct hat immer die Größe von

    Code
    1. $structlen = ($iBitrate / 8 ) * $i100msSampleCount * $iChannels

    , das ist beim Start des Programms festgelegt.

    Hups, ich habe gerade bemerkt, dass die Länge der TEXT(!)-Daten innerhalb der Struct, also die Stringlänge natürlich unterschiedlich ist. Innerhalb der Struct werden die Daten nicht gelöscht sondern nur überschrieben und somit beim Auslesen wird "der Rest" hinter dem eigentlichen String mitgezählt. Weil dort ein Textende, also CHR(0) fehlt. Wird geändert.


    Code
    1. $binarycode ="0x8B7424048B7C24088B54240CC1EA028954240C31D28B1C960FBAE31F7306F7D343B02DAA525189D8BB0A00000031C931D2F7F3665280C10109C075F366580C30AAE2F9C6077C83C701595A423B54240C72C3B000AA89D0C3"

    fixt das. Bei _int2string also etwa Zeile 280 austauschen.