From 66fa3b7f49e7c3ced6e5dc15577986e0c10eb0bb Mon Sep 17 00:00:00 2001 From: Fredrik Hernqvist Date: Tue, 9 Sep 2025 14:09:05 +0200 Subject: [PATCH 1/7] Add AudioPlayoutStats interface to spec --- index.bs | 260 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) diff --git a/index.bs b/index.bs index 061124980..af5508b15 100644 --- a/index.bs +++ b/index.bs @@ -663,6 +663,8 @@ The interfaces defined are: {{AudioNode}} which applies a non-linear waveshaping effect for distortion and other more subtle warming effects. +* An {{AudioPlayoutStats}} interface, which provides statistics about the audio played from the {{AudioContext}}. + There are also several features that have been deprecated from the Web Audio API but not yet removed, pending implementation experience of their replacements: @@ -1488,6 +1490,7 @@ interface AudioContext : BaseAudioContext { [SecureContext] readonly attribute (DOMString or AudioSinkInfo) sinkId; attribute EventHandler onsinkchange; attribute EventHandler onerror; + [SameObject] readonly attribute AudioPlayoutStats playoutStats; AudioTimestamp getOutputTimestamp (); Promise resume (); Promise suspend (); @@ -1533,6 +1536,10 @@ and to allow it only when the {{AudioContext}}'s [=relevant global object=] has :: An ordered list to store pending {{Promise}}s created by {{AudioContext/resume()}}. It is initially empty. + + : [[playout stats]] + :: + A slot where an instance of {{AudioPlayoutStats}} can be stored. It is initially null.

@@ -1769,6 +1776,19 @@ Attributes

the context is {{AudioContextState/running}}. * When the operating system reports an audio device malfunction. + : playoutStats + :: + An instance of {{AudioPlayoutStats}} for this {{AudioContext}}. + +
+ When accessing this attribute, run the following steps: + + 1. If the {{[[playout stats]]}} slot is null, construct a new {{AudioPlayoutStats}} object with [=this=] as the argument, and store it in {{[[playout stats]]}}. + + 1. Return the value of the {{[[playout stats]]}} internal slot. +
+ +

@@ -11536,6 +11556,246 @@ context.audioWorklet.addModule('vumeter-processor.js').then(() => { }); +

+The {{AudioPlayoutStats}} Interface

+ +Provides audio underrun and latency statistics for audio played through the {{AudioContext}}. + +Audio underruns (also commonly called glitches) are gaps in the audio +playout which occur when the audio pipeline cannot deliver audio on time. +Underruns (often manifesting as audible "clicks" in the playout) are bad +for the user experience, so if any of these occur it +can be useful for the application to be able to detect this and possibly +take some action to improve the playout. + +{{AudioPlayoutStats}} is a dedicated object for audio stats reporting; +it reports audio underrun and playout latency statistics for the {{AudioContext's}} playout path via +{{AudioDestinationNode}} and the associated output device. This allows +applications to measure underruns occurring due to underperforming +AudioWorklets as well as underruns and latency originating in the playout +path between the {{AudioDestinationNode}} and the output device. + +Underruns are defined in terms of [=underrun frames=] and [=underrun events=]: +- An underrun frame is an audio frame played by the output device that was not provided by the playout path. + This happens when the playout path fails to provide audio frames + to the output device on time, in which case underrun frames will be played (underrun frames are typically silence). + This typically only happens if the pipeline is underperforming. + This includes underrun situations that happen for reasons unrelated to WebAudio/{{AudioWorklet}}s. +- When an [=underrun frame=] is played after a non-underrun frame, we consider this an underrun event. + That is, multiple consecutive [=underrun frames=] will count as a single [=underrun event=]. + +
+[Exposed=Window, SecureContext]
+interface AudioPlayoutStats {
+    constructor (AudioContext context);
+    readonly attribute double underrunDuration;
+    readonly attribute unsigned long underrunEvents;
+    readonly attribute double totalDuration;
+    readonly attribute double averageLatency;
+    readonly attribute double minimumLatency;
+    readonly attribute double maximumLatency;
+    undefined resetLatency();
+    [Default] object toJSON();
+};
+
+ +{{AudioPlayoutStats}} has the following internal slots: + +
+ : [[audio context]] + :: + The {{AudioContext}} that this instance of {{AudioPlayoutStats}} is associated with. + + : [[underrun duration]] + :: + The total duration in seconds of [=underrun frames=] that {{[[audio context]]}} has played as of the last stat update, a double. Initialized to 0. + + : [[underrun events]] + :: + The total number of [=underrun events=] that has occurred in playout by {{[[audio context]]}} as of the last stat update, an int. Initialized to 0. + + : [[total duration]] + :: + The total duration in seconds of all frames (including [=underrun frames=]) that {{[[audio context]]}} has played as of the last stat update, a double. Initialized to 0. + + : [[average latency]] + :: + The average playout latency in seconds of frames played by {{[[audio context]]}} over the currently tracked interval, a double. Initialized to 0. + + : [[minimum latency]] + :: + The minimum playout latency in seconds of frames played by {{[[audio context]]}} over the currently tracked interval, a double. Initialized to 0. + + : [[maximum latency]] + :: + The maximum playout latency in seconds of frames played by {{[[audio context]]}} over the currently tracked interval, a double. Initialized to 0. + + : [[latency reset time]] + :: + The time when the latency statistics were last reset, a {{DOMHighResTimeStamp}}. +
+ +

+Constructors

+ +
+ : AudioPlayoutStats(context) + :: + Run the following steps: + 1. Set {{[[audio context]]}} to context. + 1. Set {{[[latency reset time]]}} to 0. + +
+            context: The {{AudioContext}} this new {{AudioPlayoutStats}} will be associated with.
+        
+
+ +

+Attributes

+ +Note: These attributes update only once per second and under specific conditions. See the update audio stats algorithm and privacy mitigations for details. + +
+ : underrunDuration + :: + Measures the duration of [=underrun frames=] played by the {{AudioContext}}, in seconds. + This metric can be used together with {{totalDuration}} to + calculate the percentage of played out media that was not provided by the {{AudioContext}}. + + Returns the value of the {{[[underrun duration]]}} internal slot. + +
+ : underrunEvents + :: + Measures the number of [=underrun events=] that have occurred during playout by the {{AudioContext}}. + + Returns the value of the {{[[underrun events]]}} internal slot. + +
+ : totalDuration + :: + Measures the total duration of all audio played by the {{AudioContext}}, in seconds. + + Returns the value of the {{[[total duration]]}} internal slot. + +
+ : averageLatency + :: + The average playout latency, in seconds, for audio played since the + last call to {{resetLatency()}}, or since the creation of the {{AudioContext}} if + {{resetLatency()}} has not been called. + + Returns the value of the {{[[average latency]]}} internal slot. + +
+ : minimumLatency + :: + The minimum playout latency, in seconds, for audio played since the + last call to {{resetLatency()}}, or since the creation of the {{AudioContext}} if + {{resetLatency()}} has not been called. + + Returns the value of the {{[[minimum latency]]}} internal slot. + +
+ : maximumLatency + :: + The maximum playout latency, in seconds, for audio played since the + last call to {{resetLatency()}}, or since the creation of the {{AudioContext}} if + {{resetLatency()}} has not been called. + + Returns the value of the {{[[maximum latency]]}} internal slot. + +

+Methods

+ +
+ : resetLatency() + :: + Sets the start of the interval that latency stats are tracked over to the current time. + When {{resetLatency}} is called, run the following steps: + + 1. Set {{[[latency reset time]]}} to {{BaseAudioContext/currentTime}}. + 1. Let currentLatency be the playout latency of the last frame played by + {{[[audio context]]}}, or 0 if no frames have been played out yet. + 1. Set {{[[average latency]]}} to currentLatency. + 1. Set {{[[minimum latency]]}} to currentLatency. + 1. Set {{[[maximum latency]]}} to currentLatency. + +

Updating the stats

+
+Once per second, execute the update audio stats algorithm: +1. If {{[[audio context]]}} is not running, abort these steps. +1. Let canUpdate be false. +1. Let document be the current [=this=]'s [=relevant global object=]'s [=associated Document=]. + If document is [=Document/fully active=] and document's [=Document/visibility state=] is `"visible"`, set canUpdate to true. +1. Let permission be the [=permission state=] for the permission associated with [="microphone"=] access. + If permission is "granted", set canUpdate to true. +1. If canUpdate is false, abort these steps. +1. Set {{[[underrun duration]]}} to the total duration of all [=underrun frames=] (in seconds) that + {{[[audio context]]}} has played since its construction. +1. Set {{[[underrun events]]}} to the number of times that {{[[audio context]]}} + has played an [=underrun frame=] after a non-underrun frame since its construction. +1. Set {{[[total duration]]}} to the total duration of all frames (in seconds) that + {{[[audio context]]}} has played since its construction. +1. Set {{[[average latency]]}} to the average playout latency (in seconds) of frames that + {{[[audio context]]}} has played since {{[[latency reset time]]}}. +1. Set {{[[minimum latency]]}} to the minimum playout latency (in seconds) of frames that + {{[[audio context]]}} has played since {{[[latency reset time]]}}. +1. Set {{[[maximum latency]]}} to the maximum playout latency (in seconds) of frames that + {{[[audio context]]}} has played since {{[[latency reset time]]}}. +
+ +

Privacy considerations of glitch stats

+ +
Risk
+Audio underrun information could be used to form a cross-site +covert channel between two cooperating sites. +One site could transmit information by intentionally causing audio glitches +(by causing very high CPU usage, for example) while the other site +could detect these glitches. +
Mitigations
+To inihibit the usage of such a covert channel, the API implements these mitigations. +- The values returned by the API should not be updated more than once per second. +- The api should be restricted to sites fulfill at least one of the following criteria: + 1. The site has obtained getUserMedia permission. + + Note: The reasoning is that if a site has obtained getUserMedia permission, it can receive glitch information or communicate efficiently through use of the microphone, making access to the information provided by {{AudioPlayoutStats}} redundant. These options include detecting glitches through gaps in the microphone signal, or communicating using human-inaudible sine waves. If microphone access is ever made safer in this regard, this condition should be reconsidered. + 1. The document is [=Document/fully active=] and its [=Document/visibility state=] is `"visible"`. + + Note: Assuming that neither cooperating site has microphone permission, this criteria ensures that the site that receives the covert signal must be visible, restricting the conditions under which the covert channel can be used. It makes it impossible for sites to communicate with each other using the covert channel while not visible. + +

Usage example

+This example shows how the {{AudioPlayoutStats}} can be used to calculate audio underrun and latency statistics, +and what the statistics might be used for. +
+var oldTotalDuration = audioContext.playoutStats.totalDuration;
+var oldUnderrunDuration = audioContext.playoutStats.underrunDuration;
+var oldUnderrunEvents = audioContext.playoutStats.underrunEvents;
+audioContext.playoutStats.resetLatency();
+
+// Wait while playing audio
+...
+
+// the number of seconds that were covered by the frames played by the output device between the two executions.
+let deltaTotalDuration = audioContext.playoutStats.totalDuration - oldTotalDuration;
+let deltaUnderrunDuration = audioContext.playoutStats.underrunDuration - oldUnderrunDuration;
+let deltaUnderrunEvents = audioContext.playoutStats.underrunEvents - oldUnderrunEvents;
+
+// underrun fraction stat over the last deltaTotalDuration seconds
+let underrunFraction = deltaUnderrunDuration / deltaTotalDuration;
+// underrun frequency stat over the last deltaTotalDuration seconds
+let underrunFrequency = deltaUnderrunEvents / deltaTotalDuration;
+// Average playout delay stat during the last deltaTotalDuration seconds
+let playoutDelay = audioContext.playoutStats.averageLatency;
+
+if (underrunFrequency > 0) {
+    // Do something to prevent audio glitches, like lowering WebAudio graph complexity
+}
+if (playoutDelay > 0.2) {
+    // Do something to reduce latency, like suggesting alternative playout methods
+}
+
+

Processing model

From 2aaed392aa46b7efc3f840a52b65c9871ccb2952 Mon Sep 17 00:00:00 2001 From: Fredrik Hernqvist Date: Thu, 18 Sep 2025 10:39:35 +0200 Subject: [PATCH 2/7] Apply suggestions from code review From @hoch Co-authored-by: Hongchan Choi --- index.bs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.bs b/index.bs index af5508b15..3543d05e7 100644 --- a/index.bs +++ b/index.bs @@ -11565,7 +11565,7 @@ Audio underruns (also commonly called glitches) are gaps in the audio playout which occur when the audio pipeline cannot deliver audio on time. Underruns (often manifesting as audible "clicks" in the playout) are bad for the user experience, so if any of these occur it -can be useful for the application to be able to detect this and possibly +can be useful for the application to be able to detect them and possibly take some action to improve the playout. {{AudioPlayoutStats}} is a dedicated object for audio stats reporting; @@ -11580,9 +11580,9 @@ Underruns are defined in terms of [=underrun frames=] and [=underrun events=]: This happens when the playout path fails to provide audio frames to the output device on time, in which case underrun frames will be played (underrun frames are typically silence). This typically only happens if the pipeline is underperforming. - This includes underrun situations that happen for reasons unrelated to WebAudio/{{AudioWorklet}}s. + This includes underrun situations that happen for reasons unrelated to WebAudio/{{AudioWorklet}}s. - When an [=underrun frame=] is played after a non-underrun frame, we consider this an underrun event. - That is, multiple consecutive [=underrun frames=] will count as a single [=underrun event=]. + That is, multiple consecutive [=underrun frames=] will count as a single [=underrun event=].
 [Exposed=Window, SecureContext]
@@ -11754,7 +11754,7 @@ One site could transmit information by intentionally causing audio glitches
 (by causing very high CPU usage, for example) while the other site
 could detect these glitches.
 
Mitigations
-To inihibit the usage of such a covert channel, the API implements these mitigations. +To inhibit the usage of such a covert channel, the API implements these mitigations. - The values returned by the API should not be updated more than once per second. - The api should be restricted to sites fulfill at least one of the following criteria: 1. The site has obtained getUserMedia permission. @@ -11762,13 +11762,13 @@ To inihibit the usage of such a covert channel, the API implements these mitigat Note: The reasoning is that if a site has obtained getUserMedia permission, it can receive glitch information or communicate efficiently through use of the microphone, making access to the information provided by {{AudioPlayoutStats}} redundant. These options include detecting glitches through gaps in the microphone signal, or communicating using human-inaudible sine waves. If microphone access is ever made safer in this regard, this condition should be reconsidered. 1. The document is [=Document/fully active=] and its [=Document/visibility state=] is `"visible"`. - Note: Assuming that neither cooperating site has microphone permission, this criteria ensures that the site that receives the covert signal must be visible, restricting the conditions under which the covert channel can be used. It makes it impossible for sites to communicate with each other using the covert channel while not visible. + Note: Assuming that neither cooperating site has microphone permission, this criterion ensures that the site that receives the covert signal must be visible, restricting the conditions under which the covert channel can be used. It makes it impossible for sites to communicate with each other using the covert channel while not visible.

Usage example

This example shows how the {{AudioPlayoutStats}} can be used to calculate audio underrun and latency statistics, and what the statistics might be used for.
-var oldTotalDuration = audioContext.playoutStats.totalDuration;
+let oldTotalDuration = audioContext.playoutStats.totalDuration;
 var oldUnderrunDuration = audioContext.playoutStats.underrunDuration;
 var oldUnderrunEvents = audioContext.playoutStats.underrunEvents;
 audioContext.playoutStats.resetLatency();

From 32ca9088795c747781f52893c45a0af08af594e9 Mon Sep 17 00:00:00 2001
From: Fredrik Hernqvist 
Date: Thu, 18 Sep 2025 11:06:24 +0200
Subject: [PATCH 3/7] Wrap text at column 80 and replace var with let in
 example code

---
 index.bs | 202 +++++++++++++++++++++++++++++++++++++------------------
 1 file changed, 137 insertions(+), 65 deletions(-)

diff --git a/index.bs b/index.bs
index 3543d05e7..447fe53e2 100644
--- a/index.bs
+++ b/index.bs
@@ -663,7 +663,8 @@ The interfaces defined are:
     {{AudioNode}} which applies a non-linear waveshaping
     effect for distortion and other more subtle warming effects.
 
-* An {{AudioPlayoutStats}} interface, which provides statistics about the audio played from the {{AudioContext}}.
+* An {{AudioPlayoutStats}} interface, which provides statistics about the audio
+    played from the {{AudioContext}}.
 
 There are also several features that have been deprecated from the
 Web Audio API but not yet removed, pending implementation experience
@@ -1539,7 +1540,8 @@ and to allow it only when the {{AudioContext}}'s [=relevant global object=] has
 
     : [[playout stats]]
     ::
-        A slot where an instance of {{AudioPlayoutStats}} can be stored. It is initially null.
+        A slot where an instance of {{AudioPlayoutStats}} can be stored. It is
+        initially null.
 

@@ -1781,9 +1783,12 @@ Attributes

An instance of {{AudioPlayoutStats}} for this {{AudioContext}}.
- When accessing this attribute, run the following steps: + When accessing this attribute, run the + following steps: - 1. If the {{[[playout stats]]}} slot is null, construct a new {{AudioPlayoutStats}} object with [=this=] as the argument, and store it in {{[[playout stats]]}}. + 1. If the {{[[playout stats]]}} slot is null, construct a new + {{AudioPlayoutStats}} object with [=this=] as the argument, and + store it in {{[[playout stats]]}}. 1. Return the value of the {{[[playout stats]]}} internal slot.
@@ -11559,7 +11564,8 @@ context.audioWorklet.addModule('vumeter-processor.js').then(() => {

The {{AudioPlayoutStats}} Interface

-Provides audio underrun and latency statistics for audio played through the {{AudioContext}}. +Provides audio underrun and latency statistics for audio played through the +{{AudioContext}}. Audio underruns (also commonly called glitches) are gaps in the audio playout which occur when the audio pipeline cannot deliver audio on time. @@ -11569,20 +11575,26 @@ can be useful for the application to be able to detect them and possibly take some action to improve the playout. {{AudioPlayoutStats}} is a dedicated object for audio stats reporting; -it reports audio underrun and playout latency statistics for the {{AudioContext's}} playout path via +it reports audio underrun and playout latency statistics for the +{{AudioContext's}} playout path via {{AudioDestinationNode}} and the associated output device. This allows applications to measure underruns occurring due to underperforming AudioWorklets as well as underruns and latency originating in the playout path between the {{AudioDestinationNode}} and the output device. Underruns are defined in terms of [=underrun frames=] and [=underrun events=]: -- An underrun frame is an audio frame played by the output device that was not provided by the playout path. +- An underrun frame is an audio frame played by the output device + that was not provided by the playout path. This happens when the playout path fails to provide audio frames - to the output device on time, in which case underrun frames will be played (underrun frames are typically silence). + to the output device on time, in which case underrun frames will be played + (underrun frames are typically silence). This typically only happens if the pipeline is underperforming. - This includes underrun situations that happen for reasons unrelated to WebAudio/{{AudioWorklet}}s. -- When an [=underrun frame=] is played after a non-underrun frame, we consider this an underrun event. - That is, multiple consecutive [=underrun frames=] will count as a single [=underrun event=]. + This includes underrun situations that happen for reasons unrelated to + WebAudio/{{AudioWorklet}}s. +- When an [=underrun frame=] is played after a non-underrun frame, we consider + this an underrun event. + That is, multiple consecutive [=underrun frames=] will count as a single + [=underrun event=].
 [Exposed=Window, SecureContext]
@@ -11604,35 +11616,49 @@ interface AudioPlayoutStats {
 
: [[audio context]] :: - The {{AudioContext}} that this instance of {{AudioPlayoutStats}} is associated with. + The {{AudioContext}} that this instance of {{AudioPlayoutStats}} is + associated with. : [[underrun duration]] :: - The total duration in seconds of [=underrun frames=] that {{[[audio context]]}} has played as of the last stat update, a double. Initialized to 0. + The total duration in seconds of [=underrun frames=] that + {{[[audio context]]}} has played as of the last stat update, a double. + Initialized to 0. : [[underrun events]] :: - The total number of [=underrun events=] that has occurred in playout by {{[[audio context]]}} as of the last stat update, an int. Initialized to 0. + The total number of [=underrun events=] that has occurred in playout by + {{[[audio context]]}} as of the last stat update, an int. Initialized + to 0. : [[total duration]] :: - The total duration in seconds of all frames (including [=underrun frames=]) that {{[[audio context]]}} has played as of the last stat update, a double. Initialized to 0. + The total duration in seconds of all frames + (including [=underrun frames=]) that {{[[audio context]]}} has played + as of the last stat update, a double. Initialized to 0. : [[average latency]] :: - The average playout latency in seconds of frames played by {{[[audio context]]}} over the currently tracked interval, a double. Initialized to 0. + The average playout latency in seconds of frames played by + {{[[audio context]]}} over the currently tracked interval, a double. + Initialized to 0. : [[minimum latency]] :: - The minimum playout latency in seconds of frames played by {{[[audio context]]}} over the currently tracked interval, a double. Initialized to 0. + The minimum playout latency in seconds of frames played by + {{[[audio context]]}} over the currently tracked interval, a double. + Initialized to 0. : [[maximum latency]] :: - The maximum playout latency in seconds of frames played by {{[[audio context]]}} over the currently tracked interval, a double. Initialized to 0. + The maximum playout latency in seconds of frames played by + {{[[audio context]]}} over the currently tracked interval, a double. + Initialized to 0. : [[latency reset time]] :: - The time when the latency statistics were last reset, a {{DOMHighResTimeStamp}}. + The time when the latency statistics were last reset, a + {{DOMHighResTimeStamp}}.

@@ -11646,35 +11672,43 @@ Constructors

1. Set {{[[latency reset time]]}} to 0.
-            context: The {{AudioContext}} this new {{AudioPlayoutStats}} will be associated with.
+            context: The {{AudioContext}} this new {{AudioPlayoutStats}} will
+            be associated with.
         

Attributes

-Note: These attributes update only once per second and under specific conditions. See the update audio stats algorithm and privacy mitigations for details. +Note: These attributes update only once per second and under specific +conditions. See the update audio stats +algorithm and privacy mitigations +for details.
: underrunDuration :: - Measures the duration of [=underrun frames=] played by the {{AudioContext}}, in seconds. + Measures the duration of [=underrun frames=] played by the + {{AudioContext}}, in seconds. This metric can be used together with {{totalDuration}} to - calculate the percentage of played out media that was not provided by the {{AudioContext}}. + calculate the percentage of played out media that was not provided by + the {{AudioContext}}. Returns the value of the {{[[underrun duration]]}} internal slot.
: underrunEvents :: - Measures the number of [=underrun events=] that have occurred during playout by the {{AudioContext}}. + Measures the number of [=underrun events=] that have occurred during + playout by the {{AudioContext}}. Returns the value of the {{[[underrun events]]}} internal slot.
: totalDuration :: - Measures the total duration of all audio played by the {{AudioContext}}, in seconds. + Measures the total duration of all audio played by the {{AudioContext}}, + in seconds. Returns the value of the {{[[total duration]]}} internal slot. @@ -11682,7 +11716,8 @@ Note: These attributes update only once per second and under specific conditions : averageLatency :: The average playout latency, in seconds, for audio played since the - last call to {{resetLatency()}}, or since the creation of the {{AudioContext}} if + last call to {{resetLatency()}}, or since the creation of the + {{AudioContext}} if {{resetLatency()}} has not been called. Returns the value of the {{[[average latency]]}} internal slot. @@ -11691,7 +11726,8 @@ Note: These attributes update only once per second and under specific conditions : minimumLatency :: The minimum playout latency, in seconds, for audio played since the - last call to {{resetLatency()}}, or since the creation of the {{AudioContext}} if + last call to {{resetLatency()}}, or since the creation of the + {{AudioContext}} if {{resetLatency()}} has not been called. Returns the value of the {{[[minimum latency]]}} internal slot. @@ -11700,7 +11736,8 @@ Note: These attributes update only once per second and under specific conditions : maximumLatency :: The maximum playout latency, in seconds, for audio played since the - last call to {{resetLatency()}}, or since the creation of the {{AudioContext}} if + last call to {{resetLatency()}}, or since the creation of the + {{AudioContext}} if {{resetLatency()}} has not been called. Returns the value of the {{[[maximum latency]]}} internal slot. @@ -11711,38 +11748,50 @@ Methods
: resetLatency() :: - Sets the start of the interval that latency stats are tracked over to the current time. + Sets the start of the interval that latency stats are tracked over to + the current time. When {{resetLatency}} is called, run the following steps: 1. Set {{[[latency reset time]]}} to {{BaseAudioContext/currentTime}}. - 1. Let currentLatency be the playout latency of the last frame played by - {{[[audio context]]}}, or 0 if no frames have been played out yet. + 1. Let currentLatency be the playout latency of the last + frame played by {{[[audio context]]}}, or 0 if no frames have been + played out yet. 1. Set {{[[average latency]]}} to currentLatency. 1. Set {{[[minimum latency]]}} to currentLatency. 1. Set {{[[maximum latency]]}} to currentLatency.

Updating the stats

-Once per second, execute the update audio stats algorithm: +Once per second, execute the +update audio stats algorithm: 1. If {{[[audio context]]}} is not running, abort these steps. 1. Let canUpdate be false. -1. Let document be the current [=this=]'s [=relevant global object=]'s [=associated Document=]. - If document is [=Document/fully active=] and document's [=Document/visibility state=] is `"visible"`, set canUpdate to true. -1. Let permission be the [=permission state=] for the permission associated with [="microphone"=] access. - If permission is "granted", set canUpdate to true. +1. Let document be the current [=this=]'s + [=relevant global object=]'s [=associated Document=]. +If document is [=Document/fully active=] and document's + [=Document/visibility state=] is `"visible"`, set canUpdate to + true. +1. Let permission be the [=permission state=] for the permission + associated with [="microphone"=] access. +If permission is "granted", set canUpdate to true. 1. If canUpdate is false, abort these steps. -1. Set {{[[underrun duration]]}} to the total duration of all [=underrun frames=] (in seconds) that - {{[[audio context]]}} has played since its construction. +1. Set {{[[underrun duration]]}} to the total duration of all +[=underrun frames=] (in seconds) that +{{[[audio context]]}} has played since its construction. 1. Set {{[[underrun events]]}} to the number of times that {{[[audio context]]}} - has played an [=underrun frame=] after a non-underrun frame since its construction. -1. Set {{[[total duration]]}} to the total duration of all frames (in seconds) that - {{[[audio context]]}} has played since its construction. -1. Set {{[[average latency]]}} to the average playout latency (in seconds) of frames that - {{[[audio context]]}} has played since {{[[latency reset time]]}}. -1. Set {{[[minimum latency]]}} to the minimum playout latency (in seconds) of frames that - {{[[audio context]]}} has played since {{[[latency reset time]]}}. -1. Set {{[[maximum latency]]}} to the maximum playout latency (in seconds) of frames that - {{[[audio context]]}} has played since {{[[latency reset time]]}}. + has played an [=underrun frame=] after a non-underrun frame since its + construction. +1. Set {{[[total duration]]}} to the total duration of all frames (in seconds) + that {{[[audio context]]}} has played since its construction. +1. Set {{[[average latency]]}} to the average playout latency (in seconds) of + frames that {{[[audio context]]}} has played since + {{[[latency reset time]]}}. +1. Set {{[[minimum latency]]}} to the minimum playout latency (in seconds) of + frames that {{[[audio context]]}} has played since + {{[[latency reset time]]}}. +1. Set {{[[maximum latency]]}} to the maximum playout latency (in seconds) of + frames that {{[[audio context]]}} has played since + {{[[latency reset time]]}}.

Privacy considerations of glitch stats

@@ -11754,32 +11803,53 @@ One site could transmit information by intentionally causing audio glitches (by causing very high CPU usage, for example) while the other site could detect these glitches.
Mitigations
-To inhibit the usage of such a covert channel, the API implements these mitigations. -- The values returned by the API should not be updated more than once per second. -- The api should be restricted to sites fulfill at least one of the following criteria: - 1. The site has obtained getUserMedia permission. +To inhibit the usage of such a covert channel, the API implements these +mitigations. +- The values returned by the API should not be updated more than once per + second. +- The api should be restricted to sites fulfill at least one of the following + criteria: + 1. The site has obtained + getUserMedia + permission. - Note: The reasoning is that if a site has obtained getUserMedia permission, it can receive glitch information or communicate efficiently through use of the microphone, making access to the information provided by {{AudioPlayoutStats}} redundant. These options include detecting glitches through gaps in the microphone signal, or communicating using human-inaudible sine waves. If microphone access is ever made safer in this regard, this condition should be reconsidered. - 1. The document is [=Document/fully active=] and its [=Document/visibility state=] is `"visible"`. - - Note: Assuming that neither cooperating site has microphone permission, this criterion ensures that the site that receives the covert signal must be visible, restricting the conditions under which the covert channel can be used. It makes it impossible for sites to communicate with each other using the covert channel while not visible. + Note: The reasoning is that if a site has obtained + getUserMedia + permission, it can receive glitch information or communicate + efficiently through use of the microphone, making access to the + information provided by {{AudioPlayoutStats}} redundant. These options + include detecting glitches through gaps in the microphone signal, or + communicating using human-inaudible sine waves. If microphone access is + ever made safer in this regard, this condition should be reconsidered. + 1. The document is [=Document/fully active=] and its + [=Document/visibility state=] is `"visible"`. + + Note: Assuming that neither cooperating site has microphone permission, + this criterion ensures that the site that receives the covert signal + must be visible, restricting the conditions under which the covert + channel can be used. It makes it impossible for sites to communicate + with each other using the covert channel while not visible.

Usage example

-This example shows how the {{AudioPlayoutStats}} can be used to calculate audio underrun and latency statistics, -and what the statistics might be used for. +This example shows how the {{AudioPlayoutStats}} can be used to calculate audio +underrun and latency statistics, and what the statistics might be used for.
 let oldTotalDuration = audioContext.playoutStats.totalDuration;
-var oldUnderrunDuration = audioContext.playoutStats.underrunDuration;
-var oldUnderrunEvents = audioContext.playoutStats.underrunEvents;
+let oldUnderrunDuration = audioContext.playoutStats.underrunDuration;
+let oldUnderrunEvents = audioContext.playoutStats.underrunEvents;
 audioContext.playoutStats.resetLatency();
 
 // Wait while playing audio
 ...
 
-// the number of seconds that were covered by the frames played by the output device between the two executions.
-let deltaTotalDuration = audioContext.playoutStats.totalDuration - oldTotalDuration;
-let deltaUnderrunDuration = audioContext.playoutStats.underrunDuration - oldUnderrunDuration;
-let deltaUnderrunEvents = audioContext.playoutStats.underrunEvents - oldUnderrunEvents;
+// the number of seconds that were covered by the frames played by the output
+// device between the two executions.
+let deltaTotalDuration =
+        audioContext.playoutStats.totalDuration - oldTotalDuration;
+let deltaUnderrunDuration =
+        audioContext.playoutStats.underrunDuration - oldUnderrunDuration;
+let deltaUnderrunEvents =
+        audioContext.playoutStats.underrunEvents - oldUnderrunEvents;
 
 // underrun fraction stat over the last deltaTotalDuration seconds
 let underrunFraction = deltaUnderrunDuration / deltaTotalDuration;
@@ -11789,10 +11859,12 @@ let underrunFrequency = deltaUnderrunEvents / deltaTotalDuration;
 let playoutDelay = audioContext.playoutStats.averageLatency;
 
 if (underrunFrequency > 0) {
-    // Do something to prevent audio glitches, like lowering WebAudio graph complexity
+    // Do something to prevent audio glitches, like lowering
+    // WebAudio graph complexity
 }
 if (playoutDelay > 0.2) {
-    // Do something to reduce latency, like suggesting alternative playout methods
+    // Do something to reduce latency, like suggesting alternative
+    // playout methods
 }
 
From af182977a8d73a084183e96c5cd334117fee6c59 Mon Sep 17 00:00:00 2001 From: Fredrik Hernqvist Date: Mon, 22 Sep 2025 16:36:48 +0200 Subject: [PATCH 4/7] Apply suggestions from code review Co-authored-by: Christoph Guttandin --- index.bs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 447fe53e2..a83e83d00 100644 --- a/index.bs +++ b/index.bs @@ -11658,7 +11658,7 @@ interface AudioPlayoutStats { : [[latency reset time]] :: The time when the latency statistics were last reset, a - {{DOMHighResTimeStamp}}. + double.

@@ -11807,7 +11807,7 @@ To inhibit the usage of such a covert channel, the API implements these mitigations. - The values returned by the API should not be updated more than once per second. -- The api should be restricted to sites fulfill at least one of the following +- The API should be restricted to sites that fulfill at least one of the following criteria: 1. The site has obtained getUserMedia From c39cef22598406b0c42216520d43776ff257162b Mon Sep 17 00:00:00 2001 From: Fredrik Hernqvist Date: Tue, 30 Sep 2025 16:40:03 +0200 Subject: [PATCH 5/7] Apply suggestions from code review Co-authored-by: Paul Adenot --- index.bs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.bs b/index.bs index a83e83d00..aaada7718 100644 --- a/index.bs +++ b/index.bs @@ -11574,9 +11574,9 @@ for the user experience, so if any of these occur it can be useful for the application to be able to detect them and possibly take some action to improve the playout. -{{AudioPlayoutStats}} is a dedicated object for audio stats reporting; +{{AudioPlayoutStats}} is a dedicated object for audio statistics reporting; it reports audio underrun and playout latency statistics for the -{{AudioContext's}} playout path via +{{AudioContext's}} playback path via {{AudioDestinationNode}} and the associated output device. This allows applications to measure underruns occurring due to underperforming AudioWorklets as well as underruns and latency originating in the playout @@ -11584,7 +11584,7 @@ path between the {{AudioDestinationNode}} and the output device. Underruns are defined in terms of [=underrun frames=] and [=underrun events=]: - An underrun frame is an audio frame played by the output device - that was not provided by the playout path. + that was not provided by the AudioContext. This happens when the playout path fails to provide audio frames to the output device on time, in which case underrun frames will be played (underrun frames are typically silence). @@ -11688,9 +11688,9 @@ for details.
: underrunDuration :: - Measures the duration of [=underrun frames=] played by the + Returns the duration of [=underrun frames=] played by the {{AudioContext}}, in seconds. - This metric can be used together with {{totalDuration}} to + NOTE: This metric can be used together with {{totalDuration}} to calculate the percentage of played out media that was not provided by the {{AudioContext}}. From 73b7409333a905097eb0540c6b8f8314f9452b2b Mon Sep 17 00:00:00 2001 From: Fredrik Hernqvist Date: Fri, 3 Oct 2025 15:44:18 +0200 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Paul Adenot --- index.bs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.bs b/index.bs index aaada7718..362110a61 100644 --- a/index.bs +++ b/index.bs @@ -11586,8 +11586,9 @@ Underruns are defined in terms of [=underrun frames=] and [=underrun events=]: - An underrun frame is an audio frame played by the output device that was not provided by the AudioContext. This happens when the playout path fails to provide audio frames - to the output device on time, in which case underrun frames will be played - (underrun frames are typically silence). + to the output device on time, in which case it will still have to play something. + + NOTE: Underrun frames are typically silence. This typically only happens if the pipeline is underperforming. This includes underrun situations that happen for reasons unrelated to WebAudio/{{AudioWorklet}}s. From dd0b3a5711deb15fa92a4c3162d8e982f9505576 Mon Sep 17 00:00:00 2001 From: Fredrik Hernqvist Date: Tue, 7 Oct 2025 15:46:03 +0200 Subject: [PATCH 7/7] Work on addressing review comments --- index.bs | 168 ++++++++++++++++++++++--------------------------------- 1 file changed, 67 insertions(+), 101 deletions(-) diff --git a/index.bs b/index.bs index 362110a61..6c5db790d 100644 --- a/index.bs +++ b/index.bs @@ -663,7 +663,7 @@ The interfaces defined are: {{AudioNode}} which applies a non-linear waveshaping effect for distortion and other more subtle warming effects. -* An {{AudioPlayoutStats}} interface, which provides statistics about the audio +* An {{AudioPlaybackStats}} interface, which provides statistics about the audio played from the {{AudioContext}}. There are also several features that have been deprecated from the @@ -1491,7 +1491,7 @@ interface AudioContext : BaseAudioContext { [SecureContext] readonly attribute (DOMString or AudioSinkInfo) sinkId; attribute EventHandler onsinkchange; attribute EventHandler onerror; - [SameObject] readonly attribute AudioPlayoutStats playoutStats; + [SameObject] readonly attribute AudioPlaybackStats playbackStats; AudioTimestamp getOutputTimestamp (); Promise resume (); Promise suspend (); @@ -1538,9 +1538,9 @@ and to allow it only when the {{AudioContext}}'s [=relevant global object=] has An ordered list to store pending {{Promise}}s created by {{AudioContext/resume()}}. It is initially empty. - : [[playout stats]] + : [[playback stats]] :: - A slot where an instance of {{AudioPlayoutStats}} can be stored. It is + A slot where an instance of {{AudioPlaybackStats}} can be stored. It is initially null.
@@ -1778,19 +1778,19 @@ Attributes

the context is {{AudioContextState/running}}. * When the operating system reports an audio device malfunction. - : playoutStats + : playbackStats :: - An instance of {{AudioPlayoutStats}} for this {{AudioContext}}. + An instance of {{AudioPlaybackStats}} for this {{AudioContext}}. -
+
When accessing this attribute, run the following steps: - 1. If the {{[[playout stats]]}} slot is null, construct a new - {{AudioPlayoutStats}} object with [=this=] as the argument, and - store it in {{[[playout stats]]}}. + 1. If the {{[[playback stats]]}} slot is null, construct a new + {{AudioPlaybackStats}} object with [=this=] as the argument, and + store it in {{[[playback stats]]}}. - 1. Return the value of the {{[[playout stats]]}} internal slot. + 1. Return the value of the {{[[playback stats]]}} internal slot.
@@ -11561,35 +11561,41 @@ context.audioWorklet.addModule('vumeter-processor.js').then(() => { }); -

-The {{AudioPlayoutStats}} Interface

+

+The {{AudioPlaybackStats}} Interface

Provides audio underrun and latency statistics for audio played through the {{AudioContext}}. -Audio underruns (also commonly called glitches) are gaps in the audio -playout which occur when the audio pipeline cannot deliver audio on time. -Underruns (often manifesting as audible "clicks" in the playout) are bad +When audio is not delivered to the playback device on time, this causes an +audio underrun. This causes a discontinuity in the played signal, which produces an +audible "click" which is commonly called a "glitch". These glitches are bad for the user experience, so if any of these occur it can be useful for the application to be able to detect them and possibly -take some action to improve the playout. +take some action to improve the playback. -{{AudioPlayoutStats}} is a dedicated object for audio statistics reporting; -it reports audio underrun and playout latency statistics for the +{{AudioPlaybackStats}} is a dedicated object for audio statistics reporting; +it reports audio underrun and playback latency statistics for the {{AudioContext's}} playback path via {{AudioDestinationNode}} and the associated output device. This allows -applications to measure underruns occurring due to underperforming -AudioWorklets as well as underruns and latency originating in the playout -path between the {{AudioDestinationNode}} and the output device. +applications to measure underruns underruns, which can occur due to the +following reasons: +- The audio graph is too complex for the system to generate audio on time, + causing underruns. +- There is some external problem causing issues. Examples of such problems are: + - Another program playing audio to the same playback device is malfunctioning. + - There is a global system CPU overload. + - The system is overloaded due to thermal throttling. Underruns are defined in terms of [=underrun frames=] and [=underrun events=]: - An underrun frame is an audio frame played by the output device that was not provided by the AudioContext. - This happens when the playout path fails to provide audio frames + This happens when the playback path fails to provide audio frames to the output device on time, in which case it will still have to play something. NOTE: Underrun frames are typically silence. - This typically only happens if the pipeline is underperforming. + + This typically only happens if the rendering graph is underperforming. This includes underrun situations that happen for reasons unrelated to WebAudio/{{AudioWorklet}}s. - When an [=underrun frame=] is played after a non-underrun frame, we consider @@ -11599,7 +11605,7 @@ Underruns are defined in terms of [=underrun frames=] and [=underrun events=]:
 [Exposed=Window, SecureContext]
-interface AudioPlayoutStats {
+interface AudioPlaybackStats {
     constructor (AudioContext context);
     readonly attribute double underrunDuration;
     readonly attribute unsigned long underrunEvents;
@@ -11612,12 +11618,12 @@ interface AudioPlayoutStats {
 };
 
-{{AudioPlayoutStats}} has the following internal slots: +{{AudioPlaybackStats}} has the following internal slots: -
+
: [[audio context]] :: - The {{AudioContext}} that this instance of {{AudioPlayoutStats}} is + The {{AudioContext}} that this instance of {{AudioPlaybackStats}} is associated with. : [[underrun duration]] @@ -11628,7 +11634,7 @@ interface AudioPlayoutStats { : [[underrun events]] :: - The total number of [=underrun events=] that has occurred in playout by + The total number of [=underrun events=] that has occurred in playback by {{[[audio context]]}} as of the last stat update, an int. Initialized to 0. @@ -11640,53 +11646,51 @@ interface AudioPlayoutStats { : [[average latency]] :: - The average playout latency in seconds of frames played by - {{[[audio context]]}} over the currently tracked interval, a double. - Initialized to 0. + The average audio output latency of the {{[[audio context]]}} over the currently tracked interval, a double. : [[minimum latency]] :: - The minimum playout latency in seconds of frames played by + The minimum playback latency in seconds of frames played by {{[[audio context]]}} over the currently tracked interval, a double. Initialized to 0. : [[maximum latency]] :: - The maximum playout latency in seconds of frames played by + The maximum playback latency in seconds of frames played by {{[[audio context]]}} over the currently tracked interval, a double. Initialized to 0. : [[latency reset time]] :: The time when the latency statistics were last reset, a - double. + double. This is in the clock domain of {{BaseAudioContext/currentTime}}.
-

+

Constructors

-
- : AudioPlayoutStats(context) +
+ : AudioPlaybackStats(context) :: Run the following steps: 1. Set {{[[audio context]]}} to context. 1. Set {{[[latency reset time]]}} to 0. -
-            context: The {{AudioContext}} this new {{AudioPlayoutStats}} will
+        
+            context: The {{AudioContext}} this new {{AudioPlaybackStats}} will
             be associated with.
         
-

+

Attributes

Note: These attributes update only once per second and under specific conditions. See the update audio stats -algorithm and privacy mitigations +algorithm and privacy mitigations for details. -
+
: underrunDuration :: Returns the duration of [=underrun frames=] played by the @@ -11697,15 +11701,15 @@ for details. Returns the value of the {{[[underrun duration]]}} internal slot. -
+
: underrunEvents :: Measures the number of [=underrun events=] that have occurred during - playout by the {{AudioContext}}. + playback by the {{AudioContext}}. Returns the value of the {{[[underrun events]]}} internal slot. -
+
: totalDuration :: Measures the total duration of all audio played by the {{AudioContext}}, @@ -11713,40 +11717,40 @@ for details. Returns the value of the {{[[total duration]]}} internal slot. -
+
: averageLatency :: - The average playout latency, in seconds, for audio played since the + The average playback latency, in seconds, for audio played since the last call to {{resetLatency()}}, or since the creation of the {{AudioContext}} if {{resetLatency()}} has not been called. Returns the value of the {{[[average latency]]}} internal slot. -
+
: minimumLatency :: - The minimum playout latency, in seconds, for audio played since the + The minimum playback latency, in seconds, for audio played since the last call to {{resetLatency()}}, or since the creation of the {{AudioContext}} if {{resetLatency()}} has not been called. Returns the value of the {{[[minimum latency]]}} internal slot. -
+
: maximumLatency :: - The maximum playout latency, in seconds, for audio played since the + The maximum playback latency, in seconds, for audio played since the last call to {{resetLatency()}}, or since the creation of the {{AudioContext}} if {{resetLatency()}} has not been called. Returns the value of the {{[[maximum latency]]}} internal slot. -

+

Methods

-
+
: resetLatency() :: Sets the start of the interval that latency stats are tracked over to @@ -11754,7 +11758,7 @@ Methods When {{resetLatency}} is called, run the following steps: 1. Set {{[[latency reset time]]}} to {{BaseAudioContext/currentTime}}. - 1. Let currentLatency be the playout latency of the last + 1. Let currentLatency be the playback latency of the last frame played by {{[[audio context]]}}, or 0 if no frames have been played out yet. 1. Set {{[[average latency]]}} to currentLatency. @@ -11784,18 +11788,18 @@ If permission is "granted", set canUpdate to true. construction. 1. Set {{[[total duration]]}} to the total duration of all frames (in seconds) that {{[[audio context]]}} has played since its construction. -1. Set {{[[average latency]]}} to the average playout latency (in seconds) of +1. Set {{[[average latency]]}} to the average playback latency (in seconds) of frames that {{[[audio context]]}} has played since {{[[latency reset time]]}}. -1. Set {{[[minimum latency]]}} to the minimum playout latency (in seconds) of +1. Set {{[[minimum latency]]}} to the minimum playback latency (in seconds) of frames that {{[[audio context]]}} has played since {{[[latency reset time]]}}. -1. Set {{[[maximum latency]]}} to the maximum playout latency (in seconds) of +1. Set {{[[maximum latency]]}} to the maximum playback latency (in seconds) of frames that {{[[audio context]]}} has played since {{[[latency reset time]]}}.
-

Privacy considerations of glitch stats

+

Privacy considerations of {{AudioPlayoutStats}}

Risk
Audio underrun information could be used to form a cross-site @@ -11803,12 +11807,12 @@ covert channel between two cooperating sites. One site could transmit information by intentionally causing audio glitches (by causing very high CPU usage, for example) while the other site could detect these glitches. -
Mitigations
+
Mitigations
To inhibit the usage of such a covert channel, the API implements these mitigations. -- The values returned by the API should not be updated more than once per +- The values returned by the API MUST not be updated more than once per second. -- The API should be restricted to sites that fulfill at least one of the following +- The API MUST be restricted to sites that fulfill at least one of the following criteria: 1. The site has obtained getUserMedia @@ -11818,7 +11822,7 @@ mitigations. getUserMedia permission, it can receive glitch information or communicate efficiently through use of the microphone, making access to the - information provided by {{AudioPlayoutStats}} redundant. These options + information provided by {{AudioPlaybackStats}} redundant. These options include detecting glitches through gaps in the microphone signal, or communicating using human-inaudible sine waves. If microphone access is ever made safer in this regard, this condition should be reconsidered. @@ -11831,44 +11835,6 @@ mitigations. channel can be used. It makes it impossible for sites to communicate with each other using the covert channel while not visible. -

Usage example

-This example shows how the {{AudioPlayoutStats}} can be used to calculate audio -underrun and latency statistics, and what the statistics might be used for. -
-let oldTotalDuration = audioContext.playoutStats.totalDuration;
-let oldUnderrunDuration = audioContext.playoutStats.underrunDuration;
-let oldUnderrunEvents = audioContext.playoutStats.underrunEvents;
-audioContext.playoutStats.resetLatency();
-
-// Wait while playing audio
-...
-
-// the number of seconds that were covered by the frames played by the output
-// device between the two executions.
-let deltaTotalDuration =
-        audioContext.playoutStats.totalDuration - oldTotalDuration;
-let deltaUnderrunDuration =
-        audioContext.playoutStats.underrunDuration - oldUnderrunDuration;
-let deltaUnderrunEvents =
-        audioContext.playoutStats.underrunEvents - oldUnderrunEvents;
-
-// underrun fraction stat over the last deltaTotalDuration seconds
-let underrunFraction = deltaUnderrunDuration / deltaTotalDuration;
-// underrun frequency stat over the last deltaTotalDuration seconds
-let underrunFrequency = deltaUnderrunEvents / deltaTotalDuration;
-// Average playout delay stat during the last deltaTotalDuration seconds
-let playoutDelay = audioContext.playoutStats.averageLatency;
-
-if (underrunFrequency > 0) {
-    // Do something to prevent audio glitches, like lowering
-    // WebAudio graph complexity
-}
-if (playoutDelay > 0.2) {
-    // Do something to reduce latency, like suggesting alternative
-    // playout methods
-}
-
-

Processing model