Angenommen, wir haben mehrere Monitore. Und wir wollten diese Monitore als Girlande verwenden. Lassen Sie sie beispielsweise gleichzeitig blinken. Oder ändern Sie die Farbe synchron nach einem intelligenten Algorithmus. Und was ist, wenn Sie es in einem Browser tun - dann können Sie Smartphones und Tablets daran anschließen. All das ist zur Hand.

Und da wir einen Browser verwenden, können Sie auch Sounddesign hinzufügen. Wenn es genau genug ist, um die Geräte rechtzeitig zu synchronisieren, können Sie die Sounds auf jedem so abspielen, als ob ein Mehrkanalsystem klingt.
Was kann bei der Synchronisierung von Web-Audio- und Gameplay-Uhren in einer Javascript-Anwendung auftreten? Wie viele verschiedene "Stunden" gibt es in javasctipt (drei!) und warum werden alle benötigt, sowie eine fertige Anwendung für node.js unter cat.
Überprüfen Sie die Uhr
Für jede bedingte Online-Girlande ist eine genaue Uhrzeitsynchronisation erforderlich. Schließlich können Sie auch zeitweise auftretende Netzwerkverzögerungen ignorieren. Es reicht aus, die Steuerbefehle mit einem Zeitstempel zu versehen und diese Befehle ein wenig "in die Zukunft" zu generieren. Auf Clients werden sie gepuffert und dann synchron und pünktlich ausgeführt.
Oder Sie können sogar noch weiter gehen - nehmen Sie den guten alten deterministischen Zufallsalgorithmus und verwenden Sie einen gemeinsamen Startwert (der vom Server einmal ausgegeben wird, wenn eine Verbindung besteht) auf allen Geräten. Wenn Sie einen solchen Startwert
zusammen mit der genauen Zeit verwenden, können Sie das Verhalten des Algorithmus auf allen Geräten vollständig vorgeben. Stellen Sie sich vor, Sie benötigen kein Netzwerk und keinen Server, um den Status eindeutig und synchron zu ändern. Seed enthält bereits die gesamte (bedingt unendliche) „Videoaufzeichnung“ von Aktionen im Voraus. Die Hauptsache ist die genaue Zeit.

Jede Methode hat ihre Anwendungsgrenzen. Bei der sofortigen Benutzereingabe ist natürlich nichts zu tun, es bleibt nur zu übertragen, wie es ist. Aber alles, was berechnet werden kann, ist zu berechnen. In meiner Implementierung verwende ich je nach Situation alle drei Ansätze.
Subjektiv "zur gleichen Zeit"
Im Idealfall sollte alles „zur selben Zeit“ klingen - für das schlechteste Paar unter den kombinierten Geräten sind nicht mehr als ± 10 ms Diskrepanz erforderlich. Sie können sich nicht auf eine solche Genauigkeit der Systemzeit verlassen, und Standardmethoden zum Synchronisieren der Zeit mithilfe des NTP-Protokolls sind im Browser nicht verfügbar. Deshalb werden wir unseren Synchronisationsserver ansteuern. Das Prinzip ist einfach: Helm "pingt" und akzeptiert "Pongs" mit dem Zeitstempel des Servers. Wenn Sie dies mehrmals hintereinander tun, können Sie den Fehler statistisch ausgleichen und die durchschnittliche Verzögerungszeit ermitteln.
Code: Berechnung der Serverzeit auf dem Client Websockets und darauf basierende Lösungen sind am besten geeignet, da sie keine Zeit zum Herstellen einer TCP-Verbindung benötigen und Sie in beide Richtungen mit ihnen "kommunizieren" können. Natürlich nicht UDP oder ICMP, aber unvergleichlich schneller als eine normale Kaltverbindung über die HTTP-API. Daher ist socket.io. Dort ist alles sehr einfach:
Code: socket.io Implementierung
Anstatt den Durchschnitt zu berechnen, wäre es schön, den Median zu berechnen - dies verbessert die Genauigkeit bei einer instabilen Verbindung. Die Wahl der Filtermethode liegt beim Leser. Ich vereinfache den Code hier bewusst zugunsten von Schaltplänen. Meine komplette Lösung finden Sie im Repository. performance.now ()
Ich möchte Sie daran erinnern, dass das
performance
eine API ist, die den Zugriff auf einen hochauflösenden Zeitgeber ermöglicht. Vergleichen Sie:
Date.now()
gibt die Anzahl der Millisekunden seit dem 1. Januar 1970 in ganzzahliger Form zurück. Das heißt, der Fehler nur beim Runden beträgt durchschnittlich 0,5 ms. Beispielsweise können Sie bei einer Subtraktionsoperation ab
bis zu 2 ms erfolglos „verlieren“. Darüber hinaus garantiert der Zeitmesser selbst historisch und konzeptionell keine hohe Genauigkeit und ist für die Arbeit mit einer größeren Zeitskala geschärft.performance.now()
gibt die Anzahl der Millisekunden seit dem Öffnen der Webseite zurück .
Dies ist eine relativ neue API, die speziell für die genaue Messung von Zeitintervallen "geschärft" wurde. Gibt einen Gleitkommawert zurück , der theoretisch einen Genauigkeitsgrad nahe an den Fähigkeiten des Betriebssystems selbst angibt.
Ich denke, diese Information ist fast allen Javascript-Entwicklern bekannt. Aber nicht jeder weiß das ...
Specter
Aufgrund des sensationellen Timing-Angriffs von Specter im Jahr 2018 geht alles so weit, dass der hochauflösende Timer künstlich aufgeraut wird, wenn es keine andere Lösung für das Schwachstellenproblem gibt. Firefox, beginnend mit Version 60, rundet den Wert dieses Timers auf eine Millisekunde und Edge noch schlimmer.
Hier ist, was
MDN sagt:
Der Zeitstempel ist nicht wirklich hochauflösend. Um Sicherheitsbedrohungen wie Spectre abzuschwächen, runden Browser die Ergebnisse derzeit in unterschiedlichem Maße ab. (Firefox hat in Firefox 60 auf 1 Millisekunde gerundet.) Einige Browser können den Zeitstempel auch leicht zufällig einstellen. Die Genauigkeit wird in zukünftigen Versionen möglicherweise wieder verbessert. Browser-Entwickler untersuchen diese Timing-Angriffe noch und wie sie am besten gemindert werden können.
Lassen Sie uns den Test durchführen und die Grafiken betrachten. Dies ist das Ergebnis des Tests für das Intervall von 10 ms:
Testcode: Zeitmessung in einem Zyklus function measureTimesLoop(length) { const d = new Array(length); const p = new Array(length); for (let i = 0; i < length; i++) { d[i] = Date.now(); p[i] = performance.now(); } return { d, p } }
Date.now()
performance.now()
Rand

StatistikenBrowserversion: 44.17763.771.0
Date.now ()
durchschnittliches Intervall: 1.0538336052202284 ms
Abweichung vom Mittelwertintervall, RMS: 0,7547819181245603 ms
Intervallmedian: 1 ms
performance.now ()
Durchschnittsintervall: 1.567100970873786 ms
Abweichung vom Mittelwertintervall, RMS: 0,6748006785171455 ms
Intervallmedian: 1,5015000000003056 ms
Firefox

StatistikenBrowserversion: 71.0
Date.now ()
Durchschnittsintervall: 1.0168350168350169 ms
Abweichung vom Mittelwertintervall, RMS: 0,21645930182417966 ms
Intervallmedian: 1 ms
performance.now ()
Durchschnittsintervall: 1.0134453781512605 ms
Abweichung vom Durchschnittsintervall, RMS: 0,1734108492762375 ms
Intervallmedian: 1 ms
Chrome

StatistikenBrowserversion: 79.0.3945.88
Date.now ()
Durchschnittsintervall: 1.02442996742671 ms
Abweichung vom Durchschnittsintervall, RMS: 0,49858684744444 ms
Intervallmedian: 1 ms
performance.now ()
Durchschnittsintervall: 0.005555847229948915 ms
Abweichung vom Durchschnittsintervall, RMS: 0,027497846727194235 ms
Intervallmedian: 0.0050000089686363935 ms
Ok, Chrome, Zoom auf 1 ms.

Chrome hält also immer noch an und die Implementierung von
performance.now()
wurde noch nicht erdrosselt und der Schritt ist wunderschön, 0,005 ms. Unter Edge ist der Timer von
performance.now()
rauer als
Date.now()
! In Firefox haben beide Timer die gleiche Millisekundengenauigkeit.
Zu diesem Zeitpunkt können bereits einige Schlussfolgerungen gezogen werden. Aber es gibt einen anderen Timer in Javascript (ohne den wir nicht auskommen können).
WebAudio API Timer
Dies ist ein etwas anderes Tier. Es wird für verzögerte Audio-Warteschlangen verwendet. Tatsache ist, dass Audio-Events (Noten abspielen, Effekte verwalten) nicht auf standardmäßigen asynchronen Javascript-Tools
setInterval
können:
setInterval
und
setTimeout
- wegen ihres zu großen Fehlers. Und dies ist nicht nur der Fehler
der Timer-
Werte (mit denen wir uns zuvor befasst haben), sondern es ist der Fehler, mit dem die Ereignismaschine Ereignisse ausführt. Und es ist auch bei Gewächshausbedingungen schon etwas um die 5-25 ms.
Grafiken für den asynchronen Fall unter dem SpoilerDas Ergebnis des Tests über einen Zeitraum von 100 ms:
Testcode: Zeitmessung in einem asynchronen Zyklus function pause(duration) { return new Promise((resolve) => { setInterval(() => { resolve(); }, duration); }); } async function measureTimesInAsyncLoop(length) { const d = new Array(length); const p = new Array(length); for (let i = 0; i < length; i++) { d[i] = Date.now(); p[i] = performance.now(); await pause(1); } return { d, p } }
Date.now()
performance.now()
Rand

StatistikenBrowserversion: 44.17763.771.0
Date.now ()
durchschnittliches Intervall: 25.595959595959595 ms
Abweichung vom Mittelwertintervall, RMS: 10.12639235162126 ms
Intervallmedian: 28 ms
performance.now ()
Durchschnittsintervall: 25.862596938775525 ms
Abweichung vom Mittelwertintervall, RMS: 10.123711255512573 ms
Intervallmedian: 27.027099999999336 ms
Firefox

StatistikenBrowserversion: 71.0
Date.now ()
Durchschnittsintervall: 1.6914893617021276 ms
Abweichung vom Mittelwertintervall, RMS: 0,6018870280772611 ms
Intervallmedian: 2 ms
performance.now ()
Durchschnittsintervall: 1.7865168539325842 ms
Abweichung vom Mittelwertintervall, RMS: 0,6442818510935484 ms
Intervallmedian: 2 ms
Chrome

StatistikenBrowserversion: 79.0.3945.88
Date.now ()
Durchschnittsintervall: 4,7878787878787888, ms
Abweichung vom Mittelwertintervall, RMS: 0,7557553886872682 ms
Intervallmedian: 5 ms
performance.now ()
Durchschnittsintervall: 4.783989898979516 ms
Abweichung vom Mittelwertintervall, RMS: 0,6483716900974945 ms
Medianintervall: 4.750000000058208 ms
Vielleicht erinnert sich jemand an die ersten experimentellen HTML-Audioanwendungen. Bevor vollwertiges WebAudio zu den Browsern kam, klangen sie alle wie ein bisschen betrunken und schlampig. Nur weil sie
setTimeout
als Sequenzer benutzt haben.
Die moderne WebAudio-API bietet dagegen eine garantierte Auflösung von bis zu 0,02 ms (Spekulation auf Basis der Abtastfrequenz von 44100Hz). Dies liegt daran, dass für die verzögerte
setTimeout
ein anderer Mechanismus verwendet wird als für
setTimeout
:
source.start(when);
Tatsächlich ist die Wiedergabe eines Audio-Samples „verzögert“. Nur um es zu verlieren "wird nicht verschoben", müssen Sie es "bis jetzt" verschieben.
source.start(audioCtx.currentTime);
Über durch Echtzeitsoftware erzeugte MusikWenn Sie eine programmsynthetisierte Melodie aus Noten spielen, müssen diese Noten vorab zur Wiedergabewarteschlange hinzugefügt werden. Dann wird die Melodie trotz aller nicht grundlegenden Einschränkungen und Unregelmäßigkeiten der Timer perfekt flüssig abgespielt.
Mit anderen Worten, die in Echtzeit synthetisierte Melodie sollte nicht in Echtzeit "erfunden" werden, sondern ein wenig im Voraus.
Ein Timer, um alle zu regieren
Da
audioCtx.currentTime
so stabil und genau ist, sollten wir es vielleicht als Hauptquelle für die relative Zeit verwenden? Lassen Sie uns den Test noch einmal durchführen.
Testcode: Messen der synchronen Zeitmessung in einem Zyklus function measureTimesInLoop(length) { const d = new Array(length); const p = new Array(length); const a = new Array(length); for (let i = 0; i < length; i++) { d[i] = Date.now(); p[i] = performance.now(); a[i] = audioCtx.currentTime * 1000; } return { d, p, a } }
Date.now()
performance.now()
audioCtx.currentTime
Rand

StatistikenBrowserversion: 44.17763.771.0
Date.now ()
Durchschnittsintervall: 1.037037037037037 ms
Abweichung vom Mittelwertintervall, RMS: 0,6166609846299806 ms
Intervallmedian: 1 ms
performance.now ()
Durchschnittsintervall: 1.5447103117505993 ms
Abweichung vom Mittelwertintervall, RMS: 0,4390514285320851 ms
Intervallmedian: 1,5015000000000782 ms
audioCtx.currentTime
Durchschnittsintervall: 2.955751134714949 ms
Abweichung vom Mittelwertintervall, RMS: 0,6193645611529503 ms
Intervallmedian: 2.902507781982422 ms
Firefox

StatistikenBrowserversion: 71.0
Date.now ()
Durchschnittsintervall: 1.005128205128205 ms
Abweichung vom Durchschnittsintervall, RMS: 0,12392867665225249 ms
Intervallmedian: 1 ms
performance.now ()
Durchschnittsintervall: 1.00513698630137 ms
Abweichung vom Durchschnittsintervall, RMS: 0,07148844433269844 ms
Intervallmedian: 1 ms
audioCtx.currentTime
Firefox aktualisiert den Audio-Timer-Wert in der Synchronisierungsschleife nicht
Chrome

StatistikenBrowserversion: 79.0.3945.88
Date.now ()
Durchschnittsintervall: 1.0207612456747406 ms
Abweichung vom Durchschnittsintervall, RMS: 0,49870223457982504 ms
Intervallmedian: 1 ms
performance.now ()
Durchschnittsintervall: 0.005414502034674972 ms
Abweichung vom Durchschnittsintervall, RMS: 0,027441293974958335 ms
Medianintervall: 0.004999999873689376 ms
audioCtx.currentTime
Durchschnittsintervall: 3.0877599266656963 ms
Abweichung vom Mittelwert, RMS: 1.1445555956407658 ms
Intervallmedian: 2.9024943310650997 ms
Grafiken für den asynchronen Fall unter dem SpoilerTestcode: Zeitmessung in einem asynchronen ZyklusDas Ergebnis des Tests über einen Zeitraum von 100 ms:
function pause(duration) { return new Promise((resolve) => { setInterval(() => { resolve(); }, duration); }); } async function measureTimesInAsyncLoop(length) { const d = new Array(length); const p = new Array(length); const a = new Array(length); for (let i = 0; i < length; i++) { d[i] = Date.now(); p[i] = performance.now(); await pause(1); } return { d, p } }
Date.now()
performance.now()
audioCtx.currentTime
Rand

StatistikenBrowserversion: 44.17763.771.0
Date.now ():
Durchschnittsintervall: 24.505050505050505 ms
Abweichung vom Mittelwertintervall: 11.513166584195204 ms
Intervallmedian: 26 ms
performance.now ():
Durchschnittsintervall: 24.50935757575754 ms
Abweichung vom Mittelwertintervall: 11.679091435527388 ms
Intervallmedian: 25.525499999999738 ms
audioCtx.currentTime:
Durchschnittsintervall: 24.76005164944396 ms
Abweichung vom Mittelwertintervall: 11.311571546205316 ms
Intervallmedian: 26.121139526367187 ms
Firefox

StatistikenBrowserversion: 71.0
Date.now ():
Durchschnittsintervall: 1,6875 ms
Abweichung vom Mittelwertintervall: 0.6663410663216448 ms
Intervallmedian: 2 ms
performance.now ():
Durchschnittsintervall: 1.7234042553191489 ms
Abweichung vom Mittelwertintervall: 0.6588877688171075 ms
Intervallmedian: 2 ms
audioCtx.currentTime:
Durchschnittsintervall: 10.158730158730123 ms
Abweichung vom Mittelwertintervall: 1.4512471655330046 ms
Medianintervall: 8.707482993195299 ms
Chrome

StatistikenBrowserversion: 79.0.3945.88
Date.now ():
Durchschnittsintervall: 4.585858585858586 ms
Abweichung vom Mittelwertintervall: 0.9102125516015199 ms
Intervallmedian: 5 ms
performance.now ():
durchschnittliches Intervall: 4.592424242424955 ms
Abweichung vom Mittelwertintervall: 0.719936993603155 ms
Intervallmedian: 4.605000001902226 ms
audioCtx.currentTime:
durchschnittliches Intervall: 10.12648022171832 ms
Abweichung vom Mittelwertintervall: 1.4508887886499262 ms
Intervallmedian: 8.707482993197118 ms
Nun, es wird nicht klappen. „Draußen“ ist dieser Timer am ungenauesten. Firefox aktualisiert den Timer-Wert in der Schleife nicht. Aber im Allgemeinen: Auflösung ist 3 ms und schlechter und spürbarer Jitter. Möglicherweise spiegelt der Wert von
audioCtx.currentTime
die Position im Ringpuffer des
audioCtx.currentTime
wider. Mit anderen Worten, es wird die minimale Zeit angezeigt, die es noch möglich ist, die Wiedergabe sicher zu verzögern.
Und was machen? Schließlich benötigen wir sowohl einen genauen Timer für die Synchronisierung mit dem Server und das Starten von Javascript-Ereignissen auf dem Bildschirm als auch einen Audio-Timer für Audio-Ereignisse!
Es stellt sich heraus, dass Sie alle Timer miteinander synchronisieren müssen:
- Client
audioCtx.currentTime
mit client performance.now()
auf dem Client. - Und client
performance.now()
mit performance.now()
serverseitig.
Synchronisiert, synchronisiert

Im Allgemeinen ist dies ziemlich lustig, wenn Sie darüber nachdenken: Sie können zwei gute Zeitquellen A und B haben, von denen jede am Ausgang sehr grob und verrauscht ist (A '= A + err
A ; B' = B + err
B ), so dass es kann selbst unbrauchbar sein. Der Unterschied d zwischen den ursprünglichen Quellen ohne Rauschen kann jedoch sehr genau wiederhergestellt werden.
Da der tatsächliche Zeitabstand zwischen den idealen Uhren konstant ist und n-mal gemessen wird, wird der Messfehler n-mal verringert. Es sei denn natürlich, die Uhr läuft mit der gleichen Geschwindigkeit.
Ja nicht synchronisiert
Die schlechte Nachricht ist, dass sie nicht mit der gleichen Geschwindigkeit fahren. Und ich spreche nicht von der Streuung der Stunden auf dem Server und auf dem Client - das ist verständlich und zu erwarten. Was noch unerwarteter ist:
audioCtx.currentTime
allmählich von
performance.now()
. Es ist im Client. Wir werden es vielleicht nicht bemerken, aber manchmal verschluckt das Audiosystem unter Last möglicherweise keine kleinen Daten und (entgegen der Natur des Ringpuffers) verschiebt sich die Audiozeit relativ zur Systemzeit. Dies kommt nicht so selten vor, es betrifft nur nicht viele Menschen. Wenn Sie beispielsweise zwei YouTube-Videos gleichzeitig auf verschiedenen Computern starten, ist es nicht so, dass die gleichzeitige Wiedergabe unterbrochen wird. Und der Punkt ist natürlich nicht in der Werbung.
Somit für einen stabilen und synchronen Betrieb. Wir müssen
regelmäßig alle Uhren miteinander überprüfen und dabei die Serverzeit als Referenz verwenden. Der Kompromiss
audioCtx.currentTime
sich aus der
audioCtx.currentTime
Messungen, die für die Mittelwertbildung verwendet werden sollen: Je genauer, aber je größer die Wahrscheinlichkeit, dass ein starker Sprung in
audioCtx.currentTime
in das Zeitfenster fällt, in dem die Werte gefiltert werden. Wenn wir dann zum Beispiel das Minutenfenster verwenden, ist die Zeit jede Minute abgelaufen. Die Auswahl an Filtern ist groß:
Exponential- ,
Median- ,
Kalman-Filter usw. Aber dieser Kompromiss ist auf jeden Fall.
Zeitfenster
audioCtx.currentTime
mit
performance.now()
synchronisieren, können Sie in einer asynchronen Schleife eine Messung durchführen, beispielsweise 100 ms, um die Benutzeroberfläche nicht zu beeinträchtigen.
Es sei angenommen, dass der Messfehler err = errA + errB = 1 + 3 = 4 ms ist
Dementsprechend können wir es in 1 Sekunde auf 0,4 ms und in 10 Sekunden auf 0,04 ms reduzieren. Eine weitere Verbesserung des Ergebnisses ist nicht sinnvoll und ein gutes Filterfenster ist: 1 - 10 Sekunden.
Bei der Netzwerksynchronisation sind Verzögerungen und Fehler bereits viel bedeutender, aber es gibt keinen scharfen Zeitsprung wie beim
audioCtx.currentTime
. Und Sie können sich erlauben, wirklich gute Statistiken zu sammeln. Immerhin kann err for ping bis zu 500 ms betragen. Und die Messungen selbst können wir nicht so oft machen.
An diesem Punkt schlage ich vor, aufzuhören. Wenn jemand interessiert war, erzähle ich Ihnen gerne, wie Sie "den Rest der Eule zeichnen" können. Aber als Teil der Geschichte über Timer denke ich, dass meine Geschichte vorbei ist.
Und ich möchte teilen, was ich habe. Trotzdem das neue Jahr.
Was ist passiert?
Haftungsausschluss: Technisch gesehen handelt es sich um eine PR-Site auf Habré, aber es handelt sich um ein vollständig gemeinnütziges Open-Source-Haustierprojekt, für das ich verspreche, niemals Werbung zu schalten oder anderweitig Geld zu verdienen. Im Gegenteil, ich habe jetzt mehr Instanzen von meinem Geld gesammelt, um einen möglichen Habraeffekt zu überleben. Deshalb bitte, gute Leute, brecht mich nicht und erreicht mich nicht. Das macht alles nur Spaß.
Frohes Neues Jahr, Habr!
Sie können die Regler drehen und Visualisierung, Musik und Audioeffekte steuern. Wenn Sie eine normale Grafikkarte haben, gehen Sie zu den Einstellungen und stellen Sie die Anzahl der Partikel auf 100% ein.
Benötigt WebAudio und WebGL.
UPD: Funktioniert nicht in Safari unter MacOS Mojave. Leider ist es nicht möglich, schnell herauszufinden, was passiert, da diese Safari nicht vorhanden ist. iOS scheint zu funktionieren.
UPD2: Wenn
snowtime.fun und
web.snowtime.fun nicht reagieren, versuchen Sie es mit der neuen
Unterdomäne habr .snowtime.fun . Er verlegte den Server in ein anderes Rechenzentrum und die alte IP wurde im DNS zwischengespeichert,
expire=1w
. :(
Aufbewahrungsort :
BitbucketBeim Schreiben dieses Artikels wurden
Macrovector / Freepik- Illustrationen verwendet.