Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FreeQueue + WASM example #255

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Prev Previous commit
Next Next commit
WebAssembly example, formatting and UI improvement
  • Loading branch information
DivyamAhuja committed Sep 4, 2022
commit 82f5489955c38bb6ba426a34f762878d0bc871e9
5 changes: 5 additions & 0 deletions src/_data/audioworklet_data.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
This example demonstrates how this library can be used as a channel
between a worker thread and AudioWorklet to send audio data.
href: free-queue/examples/simple-passthrough/
- title: Hello WebAssembly
DivyamAhuja marked this conversation as resolved.
Show resolved Hide resolved
description: Example of using FreeQueue with WebAssembly.
DivyamAhuja marked this conversation as resolved.
Show resolved Hide resolved
This example uses a mp3 decoding library to play load and play song
from WebAssembly.
href: free-queue/examples/hello-webassembly/

- title: Migration from ScriptProcessorNode
entries:
Expand Down
2 changes: 1 addition & 1 deletion src/_data/build_info.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"3.0.2","revision":"3eb4553","lastUpdated":"2022-08-20","copyrightYear":2022}
{"version":"3.0.2","revision":"d20a9e8","lastUpdated":"2022-09-04","copyrightYear":2022}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ to_root_dir: ../../../../
target="_blank">Chrome Developers Article: Audio Worklet Design Pattern</a>
for more details.</p>

<p> TODO: Write an article about this pattern.</p>
<div class="demo-box">
<button id="toogle" disabled>TOOGLE</button>
<button id="toogle" disabled>START</button>
{% include to_root_dir + "_includes/example-source-code.njk" %}
</div>

Expand Down
37 changes: 25 additions & 12 deletions src/audio-worklet/free-queue/examples/hello-webassembly/main.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import FreeQueue from "../../src/free-queue.js";
import ExampleModule from "./build/example.js";
import exampleModule from "./build/example.js";

const toogleButton = document.getElementById("toogle");
toogleButton.disabled = false;

let audioContext;
let playing = false;
let isPlaying = false;

/**
* Function to create and initialize AudioContext.
* @param {FreeQueue} queue FreeQueue instance to pass to AudioWorklet.
* @returns {Promise<AudioContext>}
*/
const createAudioContext = async (queue) => {
Expand All @@ -34,7 +35,7 @@ const createAudioContext = async (queue) => {

(async () => {
// Load WebAssembly Module
DivyamAhuja marked this conversation as resolved.
Show resolved Hide resolved
const Module = await ExampleModule({
const Module = await exampleModule({
locateFile: (path, prefix) => {
if (path.endsWith(".data")) return "./build/" + path;
return prefix + path;
Expand All @@ -45,12 +46,20 @@ const createAudioContext = async (queue) => {
const queuePointer = Module._getFreeQueue();

// Get addresses of data members of queue.
DivyamAhuja marked this conversation as resolved.
Show resolved Hide resolved
let GetFreeQueuePointerByMember = Module.cwrap("GetFreeQueuePointerByMember", "number", ["number", "string"]);
let getFreeQueuePointerByMember = Module.cwrap(
"GetFreeQueuePointerByMember",
DivyamAhuja marked this conversation as resolved.
Show resolved Hide resolved
"number",
["number", "string"]
);

let bufferLengthPointer = GetFreeQueuePointerByMember(queuePointer, "buffer_length");
let channelCountPointer = GetFreeQueuePointerByMember(queuePointer, "channel_count");
let statePointer = GetFreeQueuePointerByMember(queuePointer, "state");
let channelsPointer = GetFreeQueuePointerByMember(queuePointer, "channel_data");
let bufferLengthPointer =
getFreeQueuePointerByMember(queuePointer, "buffer_length");
let channelCountPointer =
getFreeQueuePointerByMember(queuePointer, "channel_count");
let statePointer =
getFreeQueuePointerByMember(queuePointer, "state");
let channelsPointer =
getFreeQueuePointerByMember(queuePointer, "channel_data");

const pointers = {
memory: Module.wasmMemory,
Expand All @@ -61,7 +70,7 @@ const createAudioContext = async (queue) => {
}
DivyamAhuja marked this conversation as resolved.
Show resolved Hide resolved

// Create queue from pointers.
DivyamAhuja marked this conversation as resolved.
Show resolved Hide resolved
const queue = FreeQueue.fromPointers(pointers)
const queue = FreeQueue.fromPointers(pointers);
/**
DivyamAhuja marked this conversation as resolved.
Show resolved Hide resolved
* Function to run, when toogle button is clicked.
* It creates AudioContext, first time button is clicked.
Expand All @@ -77,12 +86,16 @@ const createAudioContext = async (queue) => {
}
}

if (!playing) {
if (!isPlaying) {
audioContext.resume();
playing = true;
isPlaying = true;
toogleButton.style.backgroundColor = 'red';
toogleButton.innerHTML = 'STOP';
} else {
audioContext.suspend();
playing = false;
isPlaying = false;
toogleButton.style.backgroundColor = 'green';
toogleButton.innerHTML = 'START';
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FRAME_SIZE } from "./constants.js";
* Worker message event handler.
* This will initialize worker with FreeQueue instance and set loop for audio
* processing.
* @param {MessageEvent} msg
*/
self.onmessage = (msg) => {
if (msg.data.type === "init") {
Expand Down
54 changes: 27 additions & 27 deletions src/audio-worklet/free-queue/src/free-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,7 @@
* In a typical pattern is that a worklet pulls the data from the queue and a
* worker renders audio data to fill in the queue.
*/

class FreeQueue {

/**
* An index set for shared state fields. Requires atomic access.
* @enum {number}
*/
States = {
/** @type {number} A shared index for reading from the queue. (consumer) */
READ: 0,
/** @type {number} A shared index for writing into the queue. (producer) */
WRITE: 1,
}

/**
* FreeQueue constructor. A shared buffer created by this constuctor
Expand All @@ -38,7 +26,7 @@ class FreeQueue {
constructor(size, channelCount = 1) {
this.states = new Uint32Array(
new SharedArrayBuffer(
Object.keys(this.States).length * Uint32Array.BYTES_PER_ELEMENT
Object.keys(FreeQueue.States).length * Uint32Array.BYTES_PER_ELEMENT
)
);
/**
Expand All @@ -63,6 +51,7 @@ class FreeQueue {
/**
* Helper function for creating FreeQueue from pointers.
* @param {FreeQueuePointers} queuePointers
*
* An object containing various pointers required to create FreeQueue
*
* interface FreeQueuePointers {
Expand All @@ -72,7 +61,7 @@ class FreeQueue {
* statePointer: number;
* channelsPointer: number;
* }
* @returns FreeQueue
* @return {FreeQueue} A FreeQueue instance created from pointers.
*/
static fromPointers(queuePointers) {
const queue = new FreeQueue(0, 0);
Expand Down Expand Up @@ -111,8 +100,8 @@ class FreeQueue {
* @return {boolean} False if the operation fails.
*/
push(input, blockLength) {
const currentRead = Atomics.load(this.states, this.States.READ);
const currentWrite = Atomics.load(this.states, this.States.WRITE);
const currentRead = Atomics.load(this.states, FreeQueue.States.READ);
const currentWrite = Atomics.load(this.states, FreeQueue.States.WRITE);
if (this._getAvailableWrite(currentRead, currentWrite) < blockLength) {
return false;
}
Expand All @@ -133,7 +122,7 @@ class FreeQueue {
}
if (nextWrite === this.bufferLength) nextWrite = 0;
}
Atomics.store(this.states, this.States.WRITE, nextWrite);
Atomics.store(this.states, FreeQueue.States.WRITE, nextWrite);
return true;
}

Expand All @@ -147,8 +136,8 @@ class FreeQueue {
* @return {boolean} False if the operation fails.
*/
pull(output, blockLength) {
const currentRead = Atomics.load(this.states, this.States.READ);
const currentWrite = Atomics.load(this.states, this.States.WRITE);
const currentRead = Atomics.load(this.states, FreeQueue.States.READ);
const currentWrite = Atomics.load(this.states, FreeQueue.States.WRITE);
if (this._getAvailableRead(currentRead, currentWrite) < blockLength) {
return false;
}
Expand All @@ -171,16 +160,16 @@ class FreeQueue {
nextRead = 0;
}
}
Atomics.store(this.states, this.States.READ, nextRead);
Atomics.store(this.states, FreeQueue.States.READ, nextRead);
return true;
}
/**
* Helper function for debugging.
* Prints currently available read and write.
*/
printAvailableReadAndWrite() {
const currentRead = Atomics.load(this.states, this.States.READ);
const currentWrite = Atomics.load(this.states, this.States.WRITE);
const currentRead = Atomics.load(this.states, FreeQueue.States.READ);
const currentWrite = Atomics.load(this.states, FreeQueue.States.WRITE);
console.log(this, {
availableRead: this._getAvailableRead(currentRead, currentWrite),
availableWrite: this._getAvailableWrite(currentRead, currentWrite),
Expand All @@ -191,14 +180,14 @@ class FreeQueue {
* @returns {number} number of samples available for read
*/
getAvailableSamples() {
const currentRead = Atomics.load(this.states, this.States.READ);
const currentWrite = Atomics.load(this.states, this.States.WRITE);
const currentRead = Atomics.load(this.states, FreeQueue.States.READ);
const currentWrite = Atomics.load(this.states, FreeQueue.States.WRITE);
return this._getAvailableRead(currentRead, currentWrite);
}
/**
*
* @param {number} size
* @returns boolean. if frame of given size is available or not.
* @returns {boolean}. if frame of given size is available or not.
*/
isFrameAvailable(size) {
return this.getAvailableSamples() >= size;
Expand Down Expand Up @@ -226,9 +215,20 @@ class FreeQueue {
for (let channel = 0; channel < this.channelCount; channel++) {
this.channelData[channel].fill(0);
}
Atomics.store(this.states, this.States.READ, 0);
Atomics.store(this.states, this.States.WRITE, 0);
Atomics.store(this.states, FreeQueue.States.READ, 0);
Atomics.store(this.states, FreeQueue.States.WRITE, 0);
}
}

/**
* An index set for shared state fields. Requires atomic access.
* @enum {number}
*/
FreeQueue.States = {
/** @type {number} A shared index for reading from the queue. (consumer) */
READ: 0,
/** @type {number} A shared index for writing into the queue. (producer) */
WRITE: 1,
}

export default FreeQueue;