Echtzeit Beat-Detection mit Bass.au3

  • Hallo!

    Ich habe mir in den letzten Tagen erfolgreich ein USB2DMX-Interface gebaut (Arduino ftw). Das funktioniert soweit auch, sogar über ein offenes Protokoll (miniDMX). So lässt sich mein Interface auch mit DMXControl oder Freestyler nutzen. Nur gefallen mir die dortigen Umsetzungen des Sound2Light-Modus nicht. Daher dachte ich: Okay, COM-UDF und Bass.au3, alles selber schreiben. Der Teil AutoIt -> DMX funktioniert auch, ich kann über die COM-UDF mein Interface einwandfrei ansprechen usw... Nur bei der Beat-Erkennung durch die Bass-UDF hapert es ein wenig. Um keine unnötige Abhängigkeit zu einem bestimmten Player zu erzeugen, wollte ich über _BASS_Record... die aktuelle Ausgabe abfangen. Bei Realtek-Chips gibt es dafür ja das "Stereomix-Aufnahmegerät". In einem anderen Projekt hat die Echtzeit-Darstellung der Wellenform mittels _BASS_EXT_ChannelGetWaveformEx auch schon einwandfrei geklappt. Auch die Beat-Erkennung mittels _BASS_FX_BPM_BeatCallbackSet funktioniert, wenn ich zuvor einen Stream aus einer Datei erstelle.


    Jetzt kommt das Problem: In Kombination, also _BASS_Record... und _BASS_FX_BPM_BeatCallbackSet, geht leider überhaupt nichts mehr. Die Bass-Funktionen schmeißen zwar keinen Fehler, aber Beats werden auch nicht mehr erkannt. Entweder habe ich ein gewaltiges Brett vor'm Kopf oder irgendwas anderes stimmt hier nicht. Vielleicht erkennt ja einer von euch den Fehler im untenstehenden Testskript... Oder kennt womöglich noch eine alternative Lösung für mein Anliegen.


    [autoit]


    #AutoIt3Wrapper_UseX64=n
    #include "Bass.au3"
    #include "BassFX.au3"
    ;~ $ghGDIPDLL = $__g_hGDIPDLL

    [/autoit][autoit][/autoit][autoit]

    OnAutoItExitRegister("_FreeBass")

    [/autoit][autoit][/autoit][autoit]

    HotKeySet("{ESC}", "_Exit")

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    _BASS_Startup()
    ___DeBug(@error, "load bass.dll")

    [/autoit][autoit][/autoit][autoit]

    _BASS_FX_Startup()
    ___DeBug(@error, "load bassfx.dll")

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    _BASS_Init(0)
    ___DeBug(@error, "bass init")

    [/autoit][autoit][/autoit][autoit]

    ;~ $sFile = FileOpenDialog("Open...", "..\audiofiles", "playable formats (*.MP3;*.MP2;*.MP1;*.OGG;*.WAV;*.AIFF;*.AIF)")
    ;~ ___Debug($sFile = "", $sFile)

    [/autoit][autoit][/autoit][autoit]

    ;~ $hStream = _BASS_StreamCreateFile(False, $sFile, 0, 0, 0)
    _BASS_RecordInit(0)
    $hStream = _BASS_RecordStart(44100, 2, 0);, $BASS_EXT_RecordProc)

    [/autoit][autoit][/autoit][autoit]

    ___DeBug(@error, "create file stream")

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    _BASS_FX_BPM_BeatCallbackSet($hStream, "_BPMBEATPROC", 0)
    ;~ ConsoleWrite('@@ Debug(' & @ScriptLineNumber & ') : _BASS_FX_BPM_BeatCallbackSet($hStream, "_BPMBEATPROC", 0) = ' & _BASS_FX_BPM_BeatCallbackSet($hStream, "_BPMBEATPROC", 0) & @CRLF & '>Error code: ' & @error & @CRLF) ;### Debug Console
    ___DeBug(@error, "set BPM Beat callback")

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    _BASS_ChannelPlay($hStream, True)

    [/autoit][autoit][/autoit][autoit]

    While _BASS_ChannelIsActive($hStream)
    Sleep(20)
    WEnd

    [/autoit][autoit][/autoit][autoit][/autoit][autoit][/autoit][autoit]

    Func _BPMBEATPROC($handle, $pos, $user)
    ConsoleWrite("Beat position: " & $pos & @CRLF)
    EndFunc ;==>_BPMBEATPROC

    [/autoit][autoit][/autoit][autoit][/autoit][autoit][/autoit][autoit][/autoit][autoit]

    Func _Exit()
    Exit
    EndFunc ;==>_Exit

    [/autoit][autoit][/autoit][autoit]

    Func _FreeBass()
    _BASS_StreamFree($hStream)
    ___DeBug(@error, "free stream")
    _BASS_Free()
    ___DeBug(@error, "free bass")
    EndFunc ;==>_FreeBass

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    Func ___DeBug($iError, $sAction)
    Switch $iError
    Case -1
    ConsoleWrite(@CRLF & "-" & $sAction & @CRLF)
    Case -2
    ConsoleWrite(@CRLF & ">" & $sAction & @CRLF)
    Case 0
    ConsoleWrite(@CRLF & "+" & $sAction & " - OK" & @CRLF)
    Case Else
    ConsoleWrite(@CRLF & "!" & $sAction & " - FAILED, @error: " & $iError & @CRLF)
    Exit
    EndSwitch
    EndFunc ;==>___DeBug

    [/autoit]


    Es handelt sich hier um eine Modifikation von "_BASS_FX_BPM_BeatCallbackSet.au3" aus dem Beispielverzeichnis der Bass-UDF.
    Ggf. muss in Zeile 25 das Aufnahmegerät angepasst werden, die Bass-UDF wird ja hoffentlich vorhanden sein.

    Lieben Gruß
    chess

  • Hi

    Wenn RecordStart ohne CallBack aufgerufen wurde, dann wird leider nichts aufgenommen!
    Es sei denn, man ruft regelmässig GetLevel oder GetData auf.

    Die CallBackFunktion $BASS_EXT_RecordProc sendet jedesmal ein True, und die Aufnahme wird fortgesetzt. - Das funktioniert z.b mit einem Encoder, aber scheinbar nicht mit der BeatDetection!

    Probier mal folgendes:
    1) Nimm eine StreamPipe

    Sollte in etwa so funktionieren:
    (Recordet Data wird an einen PushStream weitergeleitet)

    AutoIt
    $hStream = _BASS_StreamCreate(44100, 2, 0, $STREAMPROC_PUSH, 0)
    $aPipe = _BASS_EXT_StreamPipeCreate($hStream, $BASS_EXT_STREAMPROC_PUSH)
    _BASS_ChannelPlay($hStream, True)
    $hRecord = _BASS_RecordStart(44100, 2, 0, $BASS_EXT_RecordProc, $aPipe[0])

    2) Oder rufe selber in einer Schleife regelmässig GetData auf und befülle damit einen Stream

    lgE

    EDIT: scheinbar gibt es verschiedene Versionen von BassEXT.dll - diese sollte funktionieren: BassEXT.7z
    frag mich aber nicht warum :whistling:


  • Hi!

    Erstmal danke für deine Rückmeldung.
    Ich habe das ganze mit der Dummy-Callback-Funktion ans Laufen bekommen, mit direkter Übergabe des Record-Handles an BeatCallbackSet. Im Test-Skript geht auch alles wunderbar, im richtigen Skript allerdings werden zufällige Abstürze produziert. Schaut ganz nach einem DLL-Fehler aus, komischer AutoIt Exit Code: !>23:03:19 AutoIt3.exe ended.rc:-1073741819

    Das mit den Pipes läuft leider auch nicht, obwohl ich noch die ein oder andere Veränderung getestet habe.

    Mit der neuen DLL und deinem Code treten bei mir leider _auch_ Probleme auf. Nicht so mein Tag, scheint mir. :D
    Ich höre ein Knistern und Rauschen und kriege zufällige Beats rein. Hört sich nach einem Verarbeitungsfehler innerhalb der DLLs an... Von der Geräuscheart her vergleichbar mit Rückkopplung. Kannst du mir eventuell mal alle in dem Beispiel genutzen DLLs (und vielleicht auch die AU3s, wenn die geändert worden sind) zukommen lassen? Meine DLLs tragen den Zeitstempel 7.12.2011.

    Liebe Grüße!

    Edit:
    Das Rauschen wird durch die Record-Funktion ausgelöst, soweit bin ich schon mal.

    Edit2:
    Okay, ein Workarround wäre, über den Systemmixer die AutoIt-Anwendung komplett auf Stumm zu stellen. Ist aber auch irgendwie nicht so toll, vor allem, da das Rauschen weiterhin Beats auslöst.

  • Auch wenn es ein Doppelpost ist - Es liegt ja eine größere Zeitspanne dazwischen und es gibt neue Informationen. :D

    Da ich die BeatCallbackSet-Funktion ums Verrecken nicht ans Laufen bringen konnte, habe ich nach Alternativen gesucht. Ich hatte die Wahl, entweder eine ganz andere Audio-Bibliothek für AutoIt nutzbar zu machen oder die Beat-Analyse selber zu schreiben, gestützt von der Bass-UDF. Ich habe mich für zweiteres entschieden. Es ist natürlich alles andere als präzise oder auch nur gut, aber für Musikstücke mit einer dauerhaften, gut hörbaren Bassline funktioniert es einigermaßen. Ich lese einfach in einer Schleife die Amplituden der jeweiligen Frequenzen aus und schaue, ob die Momentanauslenkung mit Einberechnung einer prozentualen Abweichung größer als die Durchschnittsauslenkung ist. Wenn ja, dann handelt es sich um einen "Beat". Dazu kommt nach jedem Beat noch eine 100ms-Pause, um Mehrfacherkennungen zu vermeiden. Wenn man nicht allzu genau hinschaut, dann sieht es halbwegs richtig aus. :D
    Noch dazu ist das Rahmenprogramm so organisiert, dass alle X ms so oder so einen Schritt in der Lichtshow weitergegangen wird.

    Hier die Erkennungsfunktion:

    [autoit]


    Func BeatAnalyze()
    Local Static $iAmpAvg = 0, $iAmpAlpha = 60, $hAmpTimer = TimerInit(), $iAmpDiff = 100

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    $hFFT = DllStructCreate("float[2048]")
    $pFFT = DllStructGetPtr($hFFT)
    _BASS_ChannelGetData($hRecord, $pFFT, $BASS_DATA_FFT2048)
    Local $aData[128]
    For $i = 0 To UBound($aData) - 1
    $aData[$i] = DllStructGetData($hFFT, 1, $i * 4)
    If $aData[$i] < 1e-4 Then $aData[$i] = 0
    Next

    [/autoit][autoit][/autoit][autoit][/autoit][autoit]

    If $aData[1] = 0 Then Return
    If TimerDiff($hAmpTimer) < $iAmpDiff Then Return
    $iAmpAvg += $aData[1]
    $iAmpAvg /= 2
    If $aData[1] >= ($iAmpAvg * (1 + $iAmpAlpha / 100)) Then
    ConsoleWrite("+> BEAT! Amplitude: " & $aData[1] & @CRLF)
    $bNextStep = True
    $hAmpTimer = TimerInit()
    Else
    ;~ ConsoleWrite("-> No Beat... Amplitude: " & $aData[1] & @CRLF)
    EndIf
    EndFunc

    [/autoit]


    Vielleicht hat ja der ein oder andere etwas mehr Erfahrung mit Musiktheorie und kann noch Verbesserungsvorschläge einbringen. ^^

  • Probier mal folgendes:

    ChannelGetData von $hRecord
    und dann mit StreamPutData in einen PushStream einfüllen.

    Diesen PushStream müsstest du allerdings vorher mit Play starten, und würde evtl. wieder Rückkopplungen verursachen.
    Es könnte ein Trick helfen:
    PushStream bleibt gestoppt!
    Nach StreamPutData gleich ChannelGetData($hStream) aufrufen (somit spielt der STream nicht, aber die DSP-Funktionen = BeatDetection sollten aufgerufen werden...)


    EDIT:

  • Ich habe keine Ahnung von Beaterkennung, aber ich kennen mich gut mit DMXContol aus. Bzw. mit allem was in diese Richtung geht u.a. GrandMA etc.
    Also wenn es fragen in diese Richtung gibt, immer her damit.
    Was ich gut finden würde wenn man aus dem erkannten Beat, wieder ein Tonsignal erzeugen könnte. Um damit die die Programm interne Beaterkennung zu triggern. Dann könnte man damit quasi die Beaterkennung "austauschen".
    Gruß Buphx

  • @eukalyptus
    Ich habe bei deinem Beispiel mal das PutData ergänzt. Leider werden - mit und ohne - auch zufällig Beats wahrgenommen. Selbst, wenn keine Anwendung irgendwelche Töne erzeugt, entstehen Matches. Dafür stürzt es nicht mehr zufällig ab. :thumbup:
    Allerdings passiert gar nichts, wenn ich das letzte GetData wegnehme. Dann sollten ja eigentlich wieder die Rückkopplungen auftreten, oder?

    @Buphx
    Interessanter Gedanke. Ich fürchte jedoch, dass das kaum umsetzbar sein wird. So viel ich weiß, kann man unter Windows nicht einfach die Tonsignale für gewisse Anwendungen "umleiten". Hört sich komisch an, wer allerdings JACK für Linux kennt, wird wissen, was gemeint ist. Mit so einer Software ginge das sicherlich relativ einfach. Allerdings gibt es JACK für Windows nicht... Man müsste also, um so eine Umleitung zu erzeugen, ein eigenes virtuelles Soundgerät erstellen. Wie das geht, ist dann aber wahrscheinlich WinSDK-Bereich.

  • PutData braucht man nicht!

    Bei einem Dummy-Stream mit Decoding-Flag ($hStream) werden die Samples im Buffer direkt von den DSPs bearbeitet.
    dh: erstes GetData = Lese Samples von Record
    zweites GetData = direkte BeatDetection an den Samples. (Ist quasi ein Set und GetData in einem)

    Nimm mal ein paar Sekunden vom StereoMix als Wav-File auf.
    Dann findest du bei den Bass-Beispielen ein Script namens "WaveForm_Beat_Detection"
    Da kannst du dir mal ansehen, ob die Detection auch richtig funktionieren würde; Weil schon etwas älter, musst du alle $ghGDIPDll noch zu $__g_hGDIPDll machen...

  • Auch dann gibt es in komplett stillen Bereichen eine Erkennung von Beats. Das ist ja definitiv falsch. Vielleicht kann man hier mit einer Signifikanzlautstärkegrenze arbeiten? Die komplett stillen Bereiche sind in der Aufnahme nebenbei durch ein leichtes Rauschen vertreten. Allerdings ändert sich das Rauschen (und die Kurve) an der Stelle der Beat-Erkennung nicht. Im Ton-Teil ist die Beat-Erkennung aber weitestgehend korrekt, ohne Fehlerkennung.

    Edit: Ich habe die Aufnahme mal angehängt.

    Edit²: Sobald ich ChannelGetLevel($hStream) aufrufe, stürzt das Skript ab. Auf $hRecord angewendet wird immer 0 zurückgegeben.

  • GetLevel wird nicht gehn, weil ein DummyStream nur virtuell ist.

    Folgende Möglichkeiten fallen mir ein:

    Falls du unbedingt GetLevel brauchst:
    Eigenen PushStream, welchen du mit PutData fütterst - Damit sollte GetLevel gehn (danach mit GetData wieder leeren, weil sich sonst der Speicher füllen könnte); Diese Variante find ich aber unschön...

    Um die Lautstärke zu verändern:
    1) Level mit $BASS_BFX_COMPRESSOR2: Mit dem Parameter "Gain" kannst du die Lautstärke der Samples verändern.
    2) Level mit $BASS_BFX_DAMP: Automatische Lautstärke (wird lauter, wenn zu leise und umgekehrt);

    BeatDetection bei Stille:
    1) BeatDetection Parameter verändern: Bandwidth u. Center frequency
    2) Mit einem LowPassFilter zuerst alle hohen Töne herausfiltern, dann BeatDetection
    3) eigene ASM-Funktion, welche alle Samples unter einem best. Wert auf Null setzt. (könnte gleichzeitig auch Get und SetLevel machen!!) <- das werd ich evtl. heute Nachmitag mal probieren

    lgE

  • Ich brauche GetLevel nicht unbedingt. Das war nur dazu gedacht, signifikante Töne von anderen zu unterscheiden, wie oben beschrieben. Also Beaterkennung bei Rauschen vermeiden.
    Vielleicht einfach zweiter Record-Stream auf das selbe Gerät, dessen Lautstärke ich dann auswerte?

    Ich spiele mal mit den beiden ersten Möglichkeiten rum. Per ASM Tondaten zu bearbeiten, geht mir ein wenig zu hoch. Aber das ist ja der Grundgedanke, den ich mit GetLevel verfolgen wollte. Ich warte bei der 3. Möglichkeit einfach mal auf dich. :D

    Grüße und Danke!

  • ASM hab ich wieder aufgegeben - hat nicht so funktioniert, wie ich dachte...

    Stattdessen hab ich folgendes probiert:

    Du musst etwas mit den Parametern $iFreq, $iGain und evtl Bandwith (Wert "0.4" in FXSetParameters) spielen
    und die Zeilen "$fLVL -= 0.05" und "If $fLevel > 0.3 Then $iBeat = 8" anpassen.