Skip to content

Commit 0bfc71e

Browse files
Choose modifying startRendering as the preferred approach (#1332)
This PR updates the progressive OfflineAudioContext explainer to reflect the latest discussions that happened in the Audio WG (discussion thread [here](WebAudio/web-audio-api#2445 (comment))). The main outcome is that now the preferred approach is to modify the behavior of `OfflineAudioContext.startRendering` in a backward-compatible manner so that it now renders audio in chunks by default.
1 parent 2e018cd commit 0bfc71e

1 file changed

Lines changed: 88 additions & 29 deletions

File tree

OfflineAudioContext/explainer.md

Lines changed: 88 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# WebAudio OfflineAudioContext.startRendering() streaming output
1+
# WebAudio OfflineAudioContext incremental rendering
22

33
## Authors:
44

@@ -13,11 +13,11 @@
1313

1414
## Introduction
1515

16-
[WebAudio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) provides a powerful and versatile API for performing audio-processing workflows in the browser. It supports complex node-based audio graphs that can be piped to system out (speakers) or an in-memory AudioBuffer for further processing, such as writing to a file. WebAudio can be used for many different workloads in the browser. An example relevant to this discussion is web-based video editors, like [clipchamp.com](https://clipchamp.com), which can use WebAudio to build up complex audio graphs based on multiple input files. These input files are composed, trimmed and processed according to a linear project timeline. The project can be previewed at realtime in the browser or exported faster-than-realtime as an .mp4.
16+
[WebAudio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) provides a powerful and versatile API for performing audio-processing workflows in the browser. It supports complex node-based audio graphs that can be piped to system out (speakers) or an in-memory AudioBuffer for further processing, such as writing to a file. WebAudio can be used for many different workloads in the browser. An example relevant to this discussion is web-based video editors, like [clipchamp.com](https://clipchamp.com), which can use WebAudio to build up complex audio graphs based on multiple input files. These input files are composed, trimmed and processed according to a linear project timeline. The project can be previewed in realtime in the browser or exported faster-than-realtime as an .mp4.
1717

1818
WebAudio works well in a realtime playback context but it is not suitable for offline context (faster-than-realtime) processing due to a limitation in the design of WebAudio's [OfflineAudioContext API](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext). The design of the API requires allocating memory to render the whole audio graph's memory up-front which can reach gigabytes of AudioBuffer data.
1919

20-
This document will propose adding a streaming offline context rendering function so that the audio graph data can be incrementally processed rather than allocating the whole audio buffer up-front.
20+
This document proposes expanding the functionality of the offline context rendering function so that the audio graph data can be incrementally processed rather than allocating the whole audio buffer up-front.
2121

2222
## User-Facing Problem
2323

@@ -31,15 +31,74 @@ A workaround to these limitations is for developers to build custom WASM audio-p
3131

3232
### Goals
3333

34-
- Allow streaming data out of a WebAudio in an offline context for rendering large audio graphs
34+
- Allow incrementally rendering data out of a OfflineAudioContext for rendering large audio graphs
3535

3636
### Non-goals
3737

3838
- Change the existing `startRendering()` behavior, this API change is additive
3939

40-
## Proposed Approach - Add `startRenderingStream()` function
40+
## Proposed Approach
4141

42-
The preferred approach is adding a new method `startRenderingStream()` that yields buffers of interleaved audio samples in a Float32Array, or another format as outlined in Open Questions. In this scenario, the user can read chunks as they arrive and consume them for storage, transcoding via WebCodecs, sending to a server, etc.
42+
We propose modifying the behavior of `startRendering()` in a backwards-compatible manner so that it always renders incrementally in chunks. With this, the current one-shot render scenario becomes a special case of the new behavior where the new `chunkSize` parameter is set to `OfflineAudioContextOptions.length`.
43+
44+
To enable this, we will need to modify `startRendering()` to accept an optional `long chunkSize` argument and `OfflineAudioContextOptions.length` will be allowed to be set to `Infinity`. With this, every call to `startRendering` will now return an `AudioBuffer` that has a maximum number of samples given by `chunkSize`. If `chunkSize` is not provided to `startRendering`, it defaults to:
45+
- The [render quantum size](https://webaudio.github.io/web-audio-api/#render-quantum-size) if `OfflineAudioContextOptions.length` is `Infinity`.
46+
- The `OfflineAudioContextOptions.length` otherwise.
47+
48+
With this proposal, all offline audio rendering is incremental by definition:
49+
- The current one-shot rendering scenario becomes a special case where `OfflineAudioContextOptions.length` is not `Infinity` and `chunkSize` is not specified (defaults to OfflineAudioContextOptions.length).
50+
- Unknown duration rendering is supported by making `OfflineAudioContextOptions.length` equal to `Infinity`.
51+
- Incremental rendering can be done by calling startRendering multiple times.
52+
53+
For the cases where there is a long ongoing one-shot render or an `Infinity`-length render that needs to stop, we propose adding a new `OfflineAudioContext.close()` that users can call to stop the rendering. Just like regular `AudioContexts`, the audio context cannot be resumed after `close` is called. Moreover, for the defined-length render case, the context will automatically transition to the `closed` state when all the audio data has been rendered.
54+
55+
Proposed interface:
56+
57+
```js
58+
partial interface OfflineAudioContext {
59+
Promise<void> close();
60+
Promise<AudioBuffer> startRendering(optional unsigned long chunkSize);
61+
}
62+
```
63+
64+
Usage example:
65+
66+
```js
67+
const context = new OfflineAudioContext({
68+
numberOfChannels: 2,
69+
sampleRate: 44100,
70+
length: Infinity
71+
});
72+
73+
// Add some nodes to build a graph...
74+
75+
// Render 5 seconds worth of data.
76+
while (context.currentTime < 5) {
77+
const buffer = await context.startRendering(/*chunkSize=*/1024);
78+
79+
processChunk(buffer);
80+
}
81+
82+
// Release resources
83+
context.close();
84+
```
85+
86+
### Pros
87+
- Maintains backwards compatibility.
88+
- Simple to reason about and implement. Callers just need to request chunks whenever they are ready to process them.
89+
- It's possible to feature detect by checking for the presence of the `close` method.
90+
- Supports unknown duration rendering.
91+
- Doesn't require integration with the Streams API.
92+
93+
### Cons
94+
- Feature detection relies on checking the presence of an adjacent method (`close`) instead of checking directly the method that renders the chunks.
95+
- Evolves the mental model for `startRendering`.
96+
97+
## Alternatives considered
98+
99+
### Alternative 1 - Add `startRenderingStream()` function
100+
101+
This alternative adds a new method `startRenderingStream()` that yields buffers of interleaved audio samples in a Float32Array, or another format as outlined in Open Questions. In this scenario, the user can read chunks as they arrive and consume them for storage, transcoding via WebCodecs, sending to a server, etc.
43102

44103
Usage example:
45104

@@ -91,29 +150,29 @@ dictionary OfflineAudioRenderingOptions {
91150

92151
partial interface OfflineAudioContext {
93152
// Immediately stops the rendering, to implement a "cancel" button when rendering
94-
// if startRenderingStream was called, this closes the stream
153+
// If startRenderingStream was called, this closes the stream
95154
// If startRendering was called, this rejects the promise
96155
Promise<void> close();
97156
// Returns a stream that yields buffers of interleaved audio samples in Float32Array or whatever format is specified
98157
Promise<ReadableStream> startRenderingStream(optional OfflineAudioRenderingOptions);
99158
};
100159
```
101160

102-
### Pros
161+
#### Pros
103162

104-
- The new capability is feature detectable because it is a new function. Compared to Alternative 1 which cannot be easily detected
105-
- Aligns well with other web streaming APIs, similar to [WebCodecs](https://streams.spec.whatwg.org/#readablestream)
106-
- Works with very large durations, no upper limit to WebAudio graph duration
163+
- The new capability is feature detectable because it is a new function. Compared to the proposed approach and alternative 2 which cannot be easily detected.
164+
- Aligns well with other web streaming APIs.
165+
- Works with very large durations, no upper limit to WebAudio graph duration.
107166

108-
### Cons
167+
#### Cons
109168

110-
- None of note
169+
- `ReadableStreams` are quite complex both for spec writers and web developers. WebCodecs has decided to decouple their specification from it (more info [here](https://docs.google.com/document/d/10S-p3Ob5snRMjBqpBf5oWn6eYij1vos7cujHoOCCCAw/edit?tab=t.0)).
111170

112-
### Output format
171+
#### Output format
113172

114173
There is an open question of what data format `startRenderingStream()` should return. The options under consideration are `AudioBuffer`, `Float32Array` planar or `Float32Array` interleaved.
115174

116-
#### `AudioBuffer`
175+
##### `AudioBuffer`
117176

118177
**Pros**
119178

@@ -123,7 +182,7 @@ There is an open question of what data format `startRenderingStream()` should re
123182

124183
- does not allow developers to BYOB (bring your own buffer) and BYOB helps developers manage memory usage, so `AudioBuffer` removes a bit of control
125184

126-
#### Planar Float32Array
185+
##### Planar Float32Array
127186

128187
**Pros**
129188

@@ -134,7 +193,7 @@ There is an open question of what data format `startRenderingStream()` should re
134193
- requires the output of `startStreamingRendering()` to return an array of `Float32Array` in planar format for each output channel
135194
- this leaves a question of what to do if only one channel is read by the consumer, i.e. what should happen to the other channel's data?
136195

137-
#### Interleaved Float32Array
196+
##### Interleaved Float32Array
138197

139198
**Pros**
140199

@@ -145,9 +204,9 @@ There is an open question of what data format `startRenderingStream()` should re
145204

146205
- None of note
147206

148-
## Alternative 1 - Modify existing `startRendering` method to allow streaming output
207+
### Alternative 2 - Modify existing `startRendering` method to allow streaming output
149208

150-
An alternative approach is to add options to the existing `startRendering()` to configure its operating mode. The mode can be set to `stream` to achieve streaming output. This is similar to the proposed approach but rather than adding a new function, it re-uses an existing function.
209+
An alternative approach is to add options to the existing `startRendering()` to configure its operating mode. The mode can be set to `stream` to achieve streaming output. This is similar to alternative 1 but rather than adding a new function, it re-uses an existing function.
151210

152211
Usage example:
153212

@@ -200,37 +259,37 @@ interface OfflineAudioContext {
200259
}
201260
```
202261

203-
### Pros
262+
#### Pros
204263

205-
- The same pros as the proposed approach
264+
- The same pros as Alternative 1
206265

207-
### Cons
266+
#### Cons
208267

209-
- The same cons at the proposed approach
210-
- It is not feature detectable, as compared to the Proposed Approach, because it only adds options dictionary to an existing function
211-
- Less explicit than the proposed approach as it overloads an existing public API function. It is safer and simpler to add a new function and not change the behaviour of an existing function
268+
- The same cons as Alternative 1
269+
- Unlike Alternative 1, it is not feature detectable because it modifies an existing function.
270+
- Less explicit than Alternative 1 as it overloads an existing public API function. It is safer and simpler to add a new function and not change the behaviour of an existing function
212271

213-
## Alternative 2 - emit `ondataavailable` events
272+
### Alternative 3 - emit `ondataavailable` events
214273

215274
Keep current `startRendering()` API but do not allocate the full `AudioBuffer`. After starting, periodically emit events on the context or a new interface such as `ondataavailable(chunk: AudioBuffer)`.
216275

217276
The user can subscribe and collect chunks for processing.
218277

219278
At the end, the API may optionally still provide a full `AudioBuffer`.
220279

221-
### Pros
280+
#### Pros
222281

223282
- Simple to integrate with existing event-driven patterns
224283

225-
### Cons
284+
#### Cons
226285

227286
- None of note but lacking support in the community discussion
228287

229288
## Stakeholder Feedback / Opposition
230289

231290
- Web community : Positive
232291

233-
The participants on the [GitHub discussion](https://github.com/WebAudio/web-audio-api/issues/2445) agree that incremental delivery of data is necessary. Either streaming chunks of rendered audio or dispatching data in bits rather a single AudioBuffer so that memory usage is bounded and the data can be processed/consumed as it is produced.
292+
The participants on the [GitHub discussion](https://github.com/WebAudio/web-audio-api/issues/2445) agree that incremental delivery of data is necessary. Either streaming chunks of rendered audio or dispatching data in bits rather than a single AudioBuffer so that memory usage is bounded and the data can be processed/consumed as it is produced.
234293

235294
## References & acknowledgements
236295

0 commit comments

Comments
 (0)