Skip to content

Commit 2d50808

Browse files
committed
Ported recorder to use a transferable ArrayBuffer pool, removed coi-serviceworker.js and SharedArrayBuffer stuff
1 parent 6193052 commit 2d50808

5 files changed

Lines changed: 55 additions & 220 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,3 @@ Try it here: https://andersnm.github.io/modulyzer/
1111
- DX7 patches: https://yamahablackboxes.com/collection/yamaha-dx7-synthesizer/patches/
1212
- Drumkits: https://filedn.com/lovhTbsn9Pz4AJR3FQeqxCF/Buzz/drumkits/ (for [Drumkit Manager 3](https://dkm3.sourceforge.net/))
1313
- Icons: https://hugeicons.com/
14-
- `SharedArrayBuffer` on localhost and GitHub Pages: https://github.com/gzuidhof/coi-serviceworker

index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<!doctype html>
22
<html lang="en">
33
<head>
4-
<script src="coi-serviceworker.js"></script>
54
<script>
65
</script>
76
<meta charset="UTF-8" />

public/coi-serviceworker.js

Lines changed: 0 additions & 146 deletions
This file was deleted.

src/audio/Recorder.ts

Lines changed: 21 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,43 @@
1-
export interface RecorderBuffer {
2-
buffer: Float32Array;
3-
bufferPosition: number;
4-
}
1+
/*
2+
v1. allocated Float32Array all the time
3+
v2. used SharedArrayBuffer, deep rabbithole of cross-origin-isolation
4+
v3. uses transferable ArrayBuffer pool
5+
*/
56

6-
const bufferSize = 8192 * 4; // size of the shared ringbuffer
7-
const outputSize = bufferSize / 4; // return quarter chunks of the ringbuffer as they're ready
7+
const bufferSize = 8192 * 4; // number of samples per buffer in the pool - must be multiple of 128
88

99
export class Recorder extends EventTarget {
1010
context: AudioContext;
1111
recordNode: AudioWorkletNode;
12-
sharedState: SharedArrayBuffer;
13-
sharedBuffers: SharedArrayBuffer[];
1412
buffers: Float32Array[];
15-
state: Int32Array;
16-
pollingInterval: number;
17-
lastReadPosition: number = 0;
18-
outBuffers: Float32Array[];
1913

2014
constructor(context: AudioContext) {
2115
super();
2216
this.context = context;
2317

24-
this.sharedState = new SharedArrayBuffer(16); // write, 3 reserved * sizeof(int32)
25-
this.sharedBuffers = [
26-
new SharedArrayBuffer(bufferSize * 4), // * sizeof(float)
27-
new SharedArrayBuffer(bufferSize * 4)
28-
];
29-
30-
this.state = new Int32Array(this.sharedState);
31-
this.buffers = [
32-
new Float32Array(this.sharedBuffers[0]),
33-
new Float32Array(this.sharedBuffers[1]),
34-
];
35-
36-
this.outBuffers = [
37-
new Float32Array(outputSize),
38-
new Float32Array(outputSize)
39-
];
18+
const bufferPool: ArrayBuffer[] = [];
19+
for (let i = 0; i < 16; i++) {
20+
bufferPool.push(new ArrayBuffer(bufferSize * Float32Array.BYTES_PER_ELEMENT));
21+
}
4022

4123
this.recordNode = new AudioWorkletNode(this.context, "record-processor");
42-
this.recordNode.port.postMessage({ type: "init", buffers: this.sharedBuffers, state: this.sharedState });
24+
this.recordNode.port.addEventListener("message", this.onMessage);
4325
this.recordNode.connect(context.destination);
4426

45-
// TODO: compute a poll interval such that a quarter of the ringbuffer is always ready
46-
this.pollingInterval = window.setInterval(this.onPoll, 90);
27+
this.recordNode.port.start();
28+
this.recordNode.port.postMessage({ type: "init", buffers: bufferPool });
4729
}
4830

49-
onPoll = () => {
50-
const writePosition = Atomics.load(this.state, 0);
51-
const available = (writePosition >= this.lastReadPosition)
52-
? writePosition - this.lastReadPosition
53-
: bufferSize - this.lastReadPosition + writePosition;
54-
55-
if (available < outputSize) return; // wait until enough data
56-
57-
const left = this.buffers[0].subarray(this.lastReadPosition, this.lastReadPosition + outputSize);
58-
const right = this.buffers[1].subarray(this.lastReadPosition, this.lastReadPosition + outputSize);
59-
60-
this.lastReadPosition += outputSize;
61-
62-
this.dispatchEvent(new CustomEvent("input", {
63-
detail: [left, right]
64-
}));
65-
}
31+
onMessage = (ev: MessageEvent<{type, buffers}>) => {
32+
if (ev.data.type === "input") {
33+
const left = new Float32Array(ev.data.buffers[0]);
34+
const right = new Float32Array(ev.data.buffers[1]);
35+
this.dispatchEvent(new CustomEvent("input", { detail: [ left, right ] }));
36+
this.recordNode.port.postMessage({ type: "recycle", buffers: ev.data.buffers }, ev.data.buffers);
37+
}
38+
};
6639

6740
destroy() {
6841
this.recordNode.port.postMessage({ type: "quit" });
69-
clearInterval(this.pollingInterval);
70-
this.pollingInterval = undefined;
7142
}
7243
}

src/audio/RecorderWorklet.ts

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
class RecordProcessor extends AudioWorkletProcessor {
22
quit: boolean = false;
3+
arrayBufferPool: ArrayBuffer[] = [];
4+
35
buffers: Float32Array[];
4-
state: Int32Array;
6+
arrayBuffers: ArrayBuffer[];
7+
writePosition: number = 0;
58

69
constructor() {
710
super();
@@ -10,13 +13,27 @@ class RecordProcessor extends AudioWorkletProcessor {
1013
this.port.start();
1114
}
1215

13-
onMessage = (ev: MessageEvent<any>) => {
16+
allocateBuffers(bufferCount: number = 2) {
17+
if (this.arrayBufferPool.length < bufferCount) {
18+
throw new Error("Out of buffers in pool");
19+
}
20+
21+
return this.arrayBufferPool.splice(0, bufferCount);
22+
}
23+
24+
postBuffers() {
25+
this.port.postMessage({ type: "input", buffers: this.arrayBuffers }, this.arrayBuffers);
26+
this.arrayBuffers = this.allocateBuffers(2);
27+
this.buffers = [ new Float32Array(this.arrayBuffers[0]), new Float32Array(this.arrayBuffers[1]) ];
28+
}
29+
30+
onMessage = (ev: MessageEvent<{type, buffers}>) => {
1431
if (ev.data.type === "init") {
15-
this.buffers = [
16-
new Float32Array(ev.data.buffers[0]),
17-
new Float32Array(ev.data.buffers[1]),
18-
];
19-
this.state = new Int32Array(ev.data.state);
32+
this.arrayBufferPool = ev.data.buffers;
33+
this.arrayBuffers = this.allocateBuffers(2);
34+
this.buffers = [ new Float32Array(this.arrayBuffers[0]), new Float32Array(this.arrayBuffers[1]) ];
35+
} else if (ev.data.type === "recycle") {
36+
this.arrayBufferPool.push(...ev.data.buffers);
2037
} else if (ev.data.type === "quit") {
2138
this.quit = true;
2239
} else {
@@ -29,36 +46,31 @@ class RecordProcessor extends AudioWorkletProcessor {
2946
return false;
3047
}
3148

32-
if (!this.buffers || !this.state) {
49+
if (!this.buffers) {
3350
return true;
3451
}
3552

3653
if (!inputs.length) {
3754
return true;
3855
}
3956

40-
let writePosition = Atomics.load(this.state, 0);
41-
4257
// Send all channels from the first input, assume only a microphone stream is connected
4358
const inputBuffers = inputs[0];
4459
const inputBufferSize = inputBuffers[0].length;
4560
const recordBufferSize = this.buffers[0].length;
4661

47-
if (writePosition + inputBufferSize <= recordBufferSize) {
48-
this.buffers[0].set(inputBuffers[0], writePosition);
49-
this.buffers[1].set(inputBuffers[1], writePosition);
50-
} else {
51-
// wrap
52-
const chunkSize = recordBufferSize - writePosition;
53-
this.buffers[0].set(inputBuffers[0].subarray(0, chunkSize), writePosition);
54-
this.buffers[1].set(inputBuffers[1].subarray(0, chunkSize), writePosition);
62+
if (this.writePosition + inputBufferSize > recordBufferSize) {
63+
throw new Error("Input buffer too large for record buffer - should never happen");
64+
}
65+
66+
this.buffers[0].set(inputBuffers[0], this.writePosition);
67+
this.buffers[1].set(inputBuffers[1], this.writePosition);
5568

56-
this.buffers[0].set(inputBuffers[0].subarray(chunkSize, inputBufferSize), 0);
57-
this.buffers[1].set(inputBuffers[1].subarray(chunkSize, inputBufferSize), 0);
69+
if (this.writePosition + inputBufferSize === recordBufferSize) {
70+
this.postBuffers();
5871
}
5972

60-
writePosition = (writePosition + inputBufferSize) % recordBufferSize;
61-
Atomics.store(this.state, 0, writePosition);
73+
this.writePosition = (this.writePosition + inputBufferSize) % recordBufferSize;
6274

6375
return true;
6476
}

0 commit comments

Comments
 (0)