Synchronisation eines Audio und Videostreams



  • Auf welchem Zielsystem läuft denn deine Software? Du könntest GetLocalTime benutzen, das löst ab Vista millisekundengenau auf.

    Argh, grad gelesen, dass es XP in einer VM sein kann.



  • Spacemuck schrieb:

    Vielen Dank für deinen Beitrag. Also Audio und Video kommen von der PSEye-Kamera, also über USB. Den Treiber dazu liefert CodeLaboratories.

    Die Software selbst wird in C# geschrieben.

    1. Fehler 🙂
    Man kann sowas mit C# zwar halbwegs hinbekommen, aber man muss aufpassen (und natürlich wissen was man tut, bzw. besser: was man nicht tun sollte). Und es wird nie vollständig ohne Aussetzer funktionieren. Irgendwann läuft der GC an, und selbst der beste concurrent GC hat eine kurze "stop the world" Phase.
    => Eine "unmanaged" Sprache wie z.B. C++ wäre hier besser.

    Ansonsten: gucken dass du nie nie nie nicht "new" aufrufen musst während das System läuft. D.h. auch keine neuen Strings erstellen etc. Alles Ringpuffer, alles voralloziert, alles nur Integers/structs modifizieren und Arrayinhalte rumkopieren. D.h. z.B. auch die Bitmap beim Kopieren nicht mit new Bitmap(original) kopieren sondern bereits eine vorallozierte Bitmap mit passenden Ausmassen wo rumliegen haben und da bloss noch die Daten rüberkopieren.

    Spacemuck schrieb:

    Video wird mit AForge.NET 2.2.5 ausgelesen. Die Frames kommen per NewFrame Event als Bitmap-Referenz, d.h. ich muss selbst noch eine Kopie des Bitmaps in meinem Buffer anlegen. Anschließend wird ein DateTime-Stamp gemacht.

    2. Fehler. Auf jeden Fall vor dem Erstellen der Kopie den Timestamp ziehen. Sofort als 1. Befehl im NewFrame-Handler.

    Spacemuck schrieb:

    Allerdings muss bei 187-Frames ungefähr alle 5,3 ms ein Stempel gemacht werden. DateTime.now liefert nur eine 10ms Auflösung. Ich hab auch nach Tests festgestellt, dass anscheinend DateTime mehrmals die gleiche Zeit für manche Frames verwendet (also mehrmals 599ms bspw.). Jedoch war dies, soweit ich mich erinnern kann, nur auf den Rechner, der Windows XP über eine VM betreibt (Dieser PC ist auch die Zielplattform, upgrade erfolgt erst später). Ich denke, den QueryPerformanceCounter zu verwenden macht da dann schon mehr Sinn (Vielleicht die Stopwatch-Klasse?).

    Die Stopwatch Klasse könnte OK sein. Da QueryPerformanceCounter aber wirklich einfach über PInvoke zu verwenden ist, würde ich es gleich mit QueryPerformanceCounter machen:
    https://msdn.microsoft.com/en-us/library/ff650674.aspx?f=255&MSPPError=-2147217396

    Spacemuck schrieb:

    Die Audiosamples kommen mittels 100ms Buffer bei 16000 Samples/Sekunde über ein DataAvailable-Event herein, werden aber von mir nicht in einen Puffer geschrieben.

    Welche(s) API/Library/Framework?
    Wichtig ist wie gesagt dass du weisst welches Sample "jetzt" ist -- also zu dem Zeitpunkt wo der Event feuert. Dann kannst du immer zurückrechnen, also wann das Sample "jetzt - N" war.

    Allgemein nochmal was die Events angeht...
    Das ist immer so ne Sache. Klingt zwar in der Theorie toll, aber man sollte zumindest nachgucken wie das alles implementiert ist.
    Es gibt z.B. eine sehr einfache Art wie man das Aufnehmen von Audiodaten machen kann. Skizziert:

    Ringpuffer für X Millisekunden anfordern
    Aufnahme mit Ringpuffer starten      -- Audio Subsystem vom OS schreibt ab jetzt direkt in den Ringpuffer
    while (Recording)
    {
        while (Schreibzeiger des Audio Subsystem ist in der 1. Pufferhälfte)
            Sleep(0.05 Sekunden)
        Daten = 1. Pufferhälfte lesen
        Feuere Event (Daten)
    
        while (Schreibzeiger des Audio Subsystem ist in der 2. Pufferhälfte)
            Sleep(0.05 Sekunden)
        Daten = 2. Pufferhälfte lesen
        Feuere Event (Daten)
    }
    

    Das ist super easy zu implementieren, und für viele Anwendungen vollkommen ausreichend.
    Dummerweise kann der Event-Handler dabei nicht wissen wo der Schreibzeiger der Audio API genau steht. Das letzte übergebene Sample könnte das aktuellste, gerade eben erst eingelesene sein, es könnte aber auch schon ordentlich alt sein (bis zu "knapp mehr als 0.05 Sekunden" alt).

    Wenn die Library die du zum Aufnehmen der Audiodaten verwendest so oder so ähnlich implementiert ist, dann gibt es nur noch eine Hoffnung. Nämlich dass die Library dir die Möglichkeit gibt im Event-Handler den Schreibzeiger des Audio Subsystem abzufragen. Und zwar so dass du das auch mit dem Datenpaket korrelieren kannst das du gerade bekommen hast. Also einfach gesagt dass du "trotzdem" irgendwie rausbekommen kannst wie alt die Samples die du gerade bekommen hast wirklich sind.

    Und bei der Video-Aufzeichnung kann es ebenfalls sein dass diesbezüglich problematischer Code existiert.

    Und nochwas: Windows 7 hat z.B. ein komplett anderes Audio-Subsystem als Windows XP. Der ganze Unterbau ist da komplett anders. Ob das bei der Aufzeichnung über "alte" APIs wie DirectSound bei Windows 7 zu Problemen führt, weiss ich nicht. Ich würde mich diesbezüglich aber auf jeden Fall mal schlaulesen.

    Bzw. wenn du Timestamp-Erfassung drinnen hast kannst du ja einfach selbst nachgucken wie gross der Jitter ist.
    Die Abstände zwischen zwei Video-Frames sollten immer identisch sein. Und bei der Audio-API kannst du auch einfach Samples mitzählen, daraus einen eigenen "Timestamp" errechnen, und dann gucken wie stark die Differenz dieses eigenen "Timestamp" und dem QueryPerformanceCounter Timestamp "jittert".

    Dann weisst du zumindest mal welcher Teil das Problem macht.



  • @DocShoe
    Wenn dann würde ich GetSystemTime vorschlagen, das hat wenigstens kein Sommerzeitproblem.
    Allerdings hat die "System-Clock" (egal ob als local oder UTC ausgelesen) das Problem, dass sie nach einer Zeitsynchronisierung kurz mit der falschen Geschwindigkeit läuft (falls der zu korrigierende Offset klein genug ist, wenn er zu gross ist wird einfach hart umgeschaltet).

    Ist also für die Messung von vergangener Zeit reichlich ungeeignet.

    GetTickCount ist billig, und wenn genau genug dann 👍
    timeGetTime ist OK
    QueryPerformanceCounter ist besser
    Andere zur Messung von vergangener Zeit geeignete Clocks unter Windows kenne ich nicht (@all: gerne ergänzen wenn etwas fehlt!).
    🙂



  • hustbaer schrieb:

    1. Fehler 🙂
    Man kann sowas mit C# zwar halbwegs hinbekommen, aber man muss aufpassen (und natürlich wissen was man tut, bzw. besser: was man nicht tun sollte). Und es wird nie vollständig ohne Aussetzer funktionieren. Irgendwann läuft der GC an, und selbst der beste concurrent GC hat eine kurze "stop the world" Phase.
    => Eine "unmanaged" Sprache wie z.B. C++ wäre hier besser.

    Ansonsten: gucken dass du nie nie nie nicht "new" aufrufen musst während das System läuft. D.h. auch keine neuen Strings erstellen etc. Alles Ringpuffer, alles voralloziert, alles nur Integers/structs modifizieren und Arrayinhalte rumkopieren. D.h. z.B. auch die Bitmap beim Kopieren nicht mit new Bitmap(original) kopieren sondern bereits eine vorallozierte Bitmap mit passenden Ausmassen wo rumliegen haben und da bloss noch die Daten rüberkopieren.

    Dass Aussetzer nicht auszuschließen sind, ist klar :). Mit dem GC hatte ich auch schon Probleme gehabt und muss quasi jedes Bild im Buffer mit Dispose() entfernen und sofort freigeben. Auf den GC zu warten endete für die Software bisher tödlich, im Sinne davon, dass der RAM einfach vollgeschrieben wird, da der GC nicht rechtzeitig anspringt. Die eingehenden Bitmaps werden mit Frame.Clone() kopiert (da wird intern new verwendet, wenn ich das richtig gelesen habe) aber anderweitig weiß ich nicht wie ich das lösen könnte.

    EDIT: Eigentlich ist das Einzige was (vermutlich) neu alloziert wird, diese Frame.Clone()-Geschichte, ansonsten nur, wenn eine Überwachung von Audio und Video nicht mehr relevant ist.

    hustbaer schrieb:

    Spacemuck schrieb:

    Video wird mit AForge.NET 2.2.5 ausgelesen. Die Frames kommen per NewFrame Event als Bitmap-Referenz, d.h. ich muss selbst noch eine Kopie des Bitmaps in meinem Buffer anlegen. Anschließend wird ein DateTime-Stamp gemacht.

    2. Fehler. Auf jeden Fall vor dem Erstellen der Kopie den Timestamp ziehen. Sofort als 1. Befehl im NewFrame-Handler.

    fixed!

    hustbaer schrieb:

    Die Stopwatch Klasse könnte OK sein. Da QueryPerformanceCounter aber wirklich einfach über PInvoke zu verwenden ist, würde ich es gleich mit QueryPerformanceCounter machen:
    https://msdn.microsoft.com/en-us/library/ff650674.aspx?f=255&MSPPError=-2147217396

    Ich habe folgenden Artikel gefunden: https://msdn.microsoft.com/en-us/library/windows/desktop/dn553408(v=vs.85).aspx
    In diesem steht, dass die Stopwatch den QPC als Basis verwendet.

    hustbaer schrieb:

    Spacemuck schrieb:

    Die Audiosamples kommen mittels 100ms Buffer bei 16000 Samples/Sekunde über ein DataAvailable-Event herein, werden aber von mir nicht in einen Puffer geschrieben.

    Welche(s) API/Library/Framework?
    Wichtig ist wie gesagt dass du weisst welches Sample "jetzt" ist -- also zu dem Zeitpunkt wo der Event feuert. Dann kannst du immer zurückrechnen, also wann das Sample "jetzt - N" war.

    Das wird schon gemacht: Ich gehe die Samples durch, prüfe die Lautstärke auf einer Schwellenüberschreitung. Falls diese Schwelle überschritten wird, wird dieses Sample "markiert" und nach der Sampleverarbeitung ein Zeitstempel gesetzt und das Event gefeuert. Anschließend wird zurückgerechnet. Das einzige Problem was ich dabei habe, oder denke ich zu haben, ist falls Audio oder Videosignale verzögert ankommen. Das Alter der Audiosamples habe ich noch nicht betrachtet. Ich schau mal ob ich was dazu finden kann.

    Die verwendete Audiolibrary ist übrigens NAudio.

    Und bei der Video-Aufzeichnung kann es ebenfalls sein dass diesbezüglich problematischer Code existiert.

    hustbaer schrieb:

    Und nochwas: Windows 7 hat z.B. ein komplett anderes Audio-Subsystem als Windows XP. Der ganze Unterbau ist da komplett anders. Ob das bei der Aufzeichnung über "alte" APIs wie DirectSound bei Windows 7 zu Problemen führt, weiss ich nicht. Ich würde mich diesbezüglich aber auf jeden Fall mal schlaulesen.

    Zumindest hat die Implementierung noch keine Probleme unter Windows 8.1 und XP gemacht. Ich hatte angefangen die Software mit dem MMSubsystem (kann leider jetzt nur auf den Namen innerhalb der Library zugreifen) zu entwerfen, musste aber dann feststellen dass dieses nicht mit Versionen < Vista kompatibel war und bin auf die WaveIn-Implementierung umgestiegen, die relativ zuverlässig erscheint.

    hustbaer schrieb:

    Bzw. wenn du Timestamp-Erfassung drinnen hast kannst du ja einfach selbst nachgucken wie gross der Jitter ist.
    Die Abstände zwischen zwei Video-Frames sollten immer identisch sein. Und bei der Audio-API kannst du auch einfach Samples mitzählen, daraus einen eigenen "Timestamp" errechnen, und dann gucken wie stark die Differenz dieses eigenen "Timestamp" und dem QueryPerformanceCounter Timestamp "jittert".

    Dann weisst du zumindest mal welcher Teil das Problem macht.

    Ok, die Idee ist dann folgende:
    Audio und Video wird gestartet. Nach jedem Durchgang der Audiosamples wird ein Zeitstempel mittels Stopwatch.GetTimestamp() gesetzt um zurück zu rechnen.

    Bei den Frames wird vor jedem Frame ein Zeitstempel gesetzt und parallel dazu noch geprüft, wie lange es bis zum nächsten Frame gedauert hat. Falls der durchschnittliche Wert überschritten wurde, summiere ich dann diese Latenz auf die Zeit bei der Ermittlung des passenden Frames auf (Ermittle die theoretische optimale Zeit und addiere dann die Latenz). Nach 1s wird die zu addierende Latenz wieder auf 0 gesetzt.



  • Spacemuck schrieb:

    Die eingehenden Bitmaps werden mit Frame.Clone() kopiert (da wird intern new verwendet, wenn ich das richtig gelesen habe) aber anderweitig weiß ich nicht wie ich das lösen könnte.

    EDIT: Eigentlich ist das Einzige was (vermutlich) neu alloziert wird, diese Frame.Clone()-Geschichte, ansonsten nur, wenn eine Überwachung von Audio und Video nicht mehr relevant ist.

    Indem du einen Rinpuffer aus mehreren vorallozierten Bitmaps hast, mit exakt dem selben Color-Format und der selben Grösse wie das was du von der Kamera bekommst.

    Aus diesem Puffer schnappst du dir die nächste zum Reinkopieren.
    Dann machst du auf die Source und auf die Destination Bitmap LockBits, und kopierst die Daten mit CopyMemory (PInvoke) rüber.
    (zu CopyMemory siehe z.B. http://stackoverflow.com/questions/15975972/copy-data-from-from-intptr-to-intptr)

    Spacemuck schrieb:

    In diesem steht, dass die Stopwatch den QPC als Basis verwendet.

    Ja, tut sie. Muss aber nicht immer so bleiben. Ist aber nicht so tragisch, kannst auch Stopwatch nehmen.

    Spacemuck schrieb:

    Das Alter der Audiosamples habe ich noch nicht betrachtet. Ich schau mal ob ich was dazu finden kann.

    Ja, das wäre genau der wichtige Punkt. Also je nachdem wie NAudio arbeitet könnte das sehr wichtig sein.

    Spacemuck schrieb:

    Zumindest hat die Implementierung noch keine Probleme unter Windows 8.1 und XP gemacht. Ich hatte angefangen die Software mit dem MMSubsystem (kann leider jetzt nur auf den Namen innerhalb der Library zugreifen) zu entwerfen, musste aber dann feststellen dass dieses nicht mit Versionen < Vista kompatibel war und bin auf die WaveIn-Implementierung umgestiegen, die relativ zuverlässig erscheint.

    Ich meine nicht dass es Probleme geben könnte im Sinn von "läuft nicht mehr". Aber es könnten eben Verzögerungen entstehen die die Qualität der Ergebnisse verschlechtern.

    Spacemuck schrieb:

    Ok, die Idee ist dann folgende:
    Audio und Video wird gestartet. Nach jedem Durchgang der Audiosamples wird ein Zeitstempel mittels Stopwatch.GetTimestamp() gesetzt um zurück zu rechnen.

    Bei den Frames wird vor jedem Frame ein Zeitstempel gesetzt und parallel dazu noch geprüft, wie lange es bis zum nächsten Frame gedauert hat. Falls der durchschnittliche Wert überschritten wurde, summiere ich dann diese Latenz auf die Zeit bei der Ermittlung des passenden Frames auf (Ermittle die theoretische optimale Zeit und addiere dann die Latenz). Nach 1s wird die zu addierende Latenz wieder auf 0 gesetzt.

    Ehrlich gesagt kann ich dir jetzt nicht ganz folgen.

    Was ich meinte ist: du kannst, indem du guckst was du bekommst und wann du es bekommst, rausfinden, ob du Daten zu spät bekommst. Und wie viel zu spät. Daraus kannst du keinen sinnvollen Offset berechnen mit dem die Timestapms der nächsten Frames/Samples korrigieren kannst. Du weisst dann nur wie schlimm das Problem bei der Audio-Erfassung ist und wie schlimm es bei der Video-Erfassung ist.

    Im Prinzip ist es relativ einfach:

    Du zählst einfach mit TotalSampleCount = wie viele Audio-Samples du seit Beginn der Aufzeichnung erhalten hast.
    Ebenso merkst du dir AT0 = die Zeit bei Beginn der Aufzeichnung (Stopwatch/QPC).
    Wenn dann ein neues Sample-Paket reinkommt, dann holst du dir als erstes mal RT = die aktuelle Zeit.
    Dann aktualisierst du TotalSampleCount .
    Und dann errechnest du die aktuelle "Audio Time" AT = AT0 + TotalSampleCount * Samplefrequenz .
    Berechnest dann die Differenz DIFF = RT - AT .
    Und speicherst diese z.B. in einem eigenen Ringpuffer ab. Den kannst du dann nach Programmende oder auf Tastendruck z.B. in ein File rausschreiben, damit du die Werte auch sehen kannst.
    Wie hoch diese Differenz ist, ist im Prinzip egal. Wichtig ist, dass sie während der Messung inetwa konstant bleibt. Bzw. sie darf sich schon ändern, aber nur ganz ganz langsam und gleichmässig. Wenn die Differenz sich z.B. über einen Zeitraum von vielen Stunden um ein paar Sekunden verändert, dann ist das vermutlich egal. Das ist kaum vermeidbar, weil QPC und die Sample-Clock der Soundkarte nie EXAKT gleich schnell sein werden.

    Das selbe kannst du dann für Video machen.

    Wenn du dann Audio und Video soweit hinbekommen hast, dass bei beiden die Differenz konstant ist, dann hast du einen konstanten Offset zwischen Audio und Video. Und DANN kannst du im Programm die "Differenz zwischen den Differenzen" verwenden um diesen konstanten Offset zu korrigieren.



  • Spacemuck schrieb:

    Ein Ansatz dachte ich wäre evtl. den Start von Video und Audio zu überwachen und jedesmal bei Beginn des Ringpuffers (also nach jedem Durchlauf) ein Zeitstempel zu setzen und so irgendwie beide Streams zu synchronisiseren. Aber so richtig kommt mir noch nicht eine passende Idee in den Sinn.

    aus diesem grund benutzt man bei filmen http://berlinliebtdich.tv/wp-content/uploads/2011/11/Kamera-Klappe1.png

    wenn das ordentlich benutzt wird, gibt es einen knall und wenn spaeter der audio und video track zusammengelegt werden, kann man das audio und video signal an dieser stelle anfangen lassen.

    wenn ab da die kamera stabile 189frame/s liefert und der ton mit z.b. 44100Hz gesamplet wird, kannst du eine lange zeit synchronitaet annehmen und die kugel bilder aussortieren.



  • @rapso: Jap, die Filmklappe kenn ich 🙂

    @beide/all: Ich muss ja nicht dauerhaft Synchronität haben, sondern sobald der Fall registriert worden ist, werden Audio/Video unterbrochen.

    @hustbaer: Mit der Samplefrequenz meinst du dann sicherlich bspw. (TotalSampleCount * 1/44100) oder?



  • Upps, ja, es muss natürlich TotalSampleCount / Samplefrequenz heissen.



  • Also bei beiden sind die Zeiten etwa gleich, ich weiß nur gerade nicht wie ich mir den Offset daraus erschließe.

    Beispiel Audio: (Stopwatch verwendet, Zeiten in ms)

    121|218|318|418|520|618|718|819|920|1017|1117|1218
    

    Beispiel Video: (Analog Audio)

    68|70|73|77|81|84|87|90|93|97|100|103|105|108|111|114|118|121|124|126|130|131|132|135|138|141|145|149|151|155|159|163|168|172|176|181|185|189|194|198|202|207|211|215|220|224|228|
    232|237|241|245|250|254|258|263|267|271|276|280|284|289|293|297|302|306|311|315|319|324|328|332|337|341|345|350|354|358|363|367|371|376|380|384|389|393|397|402|406|410|415|419|423|428|
    432|436|441|445|449|454|458|462|467|471|475|480|484|488|493|497|501|506|510|514|519|523|527|532|536|541|545|549|553|558|562|566|571|575|579|584|588|592|597|601|605|610|614|618|623|627|
    631|636|640|644|649|653|657|662|666|671|675|679|684|688|692|697|701|705|710|714|718|723|727|731|736|740|744|749|753|757|762|766|770|775|779|783|788|792|796|801|805|809|
    814|818|822|827|831|835|840|844|848|853|857|861|866|870|874|879|883|888|892|896|901|905|909|913|918|922|926|931|935|939|944|948|952|957|961|965|970|974|978|983|988|992|996|1000|
    1005|1009|1013|1018|1022|1026|1030|1035|1039|1044|1048|1052|1057|1061|1065|1069|1074|1078|1083|1087|1091|1095|1100|1104|1109|1113|1118|1122|1126|1130|1135|1139|1143|
    1148|1152|1156|1161|1165|1169|1174|1178|1182|1187|1191|119
    5|1200|1204|1208|1213|1217|1221|
    


  • Ich mach hier mehr oder weniger einen push...
    Mein Code für Audio wäre folgender:

    DateTimeMilli = Stoptimer.Time; Zeit holen
      TotalSampleCount += e.BytesRecorded / 2; //Samples: nur ein Kanal benötigt
    
      //hustbaer: Deine Formel
      long AudioTime = StartTimeT0 + TotalSampleCount / SampleRate; 
    
      //Diff = RT - AT ::> DateTimeMilli - AudioTime
      m_Diff[DifferenceIndex] = DateTimeMilli - AudioTime;
    
      DifferenceIndex = (DifferenceIndex + 1) % m_Diff.Length; //Ringpuffer weiterschalten
    
    ...hier ist nur Code zum Prüfen, ob die Lautstärkeschwelle überschritten wurde
    

    Analog für Video:

    m_ImageTime[m_sIndex] = Stoptimer.Time;         //Zeitstempel des Bildes machen
      m_Camera.TotalFrameCount++;
    
      float TotalDivFps = (float)m_Camera.TotalFrameCount / (float)FPS;
      long VideoTime = m_Camera.StartTimeT0 + (long)TotalDivFps;
      m_Camera.Difference[m_Camera.DiffIndex] = m_ImageTime[m_sIndex] - VideoTime; //Diff = RT - VT = m_ImageTime[m_sIndex] - VideoTime
      m_Camera.DiffIndex = (m_Camera.DiffIndex + 1) % m_Camera.Difference.Length; //Ringpuffer weiterschalten
    

    Ich würde jetzt den aktuellen Differenzwert mit dem vorherigen vergleichen.
    Anschließend nehme ich die Frames pro Sekunde und berechne wie lange einer bräuchte.
    Die Differenz des aktuellen und des vorherigen Wertes addiere ich dem aktuellen Video/Audiotimewert hinzu oder subtrahiere diesen (je nachdem wie lange ein
    Frame dauern müsste, also 1/FPS bspw. = 5ms). Dann hätte ich doch einen relativ gleichen Abstand.

    Ist dieser Ansatz korrekt?


Anmelden zum Antworten