diff --git a/.eleventy.js b/.eleventy.js
index f62a0be5c..f8b5ab24a 100644
--- a/.eleventy.js
+++ b/.eleventy.js
@@ -81,6 +81,7 @@ module.exports = function(eleventyConfig) {
'src/README.md',
'src/sitemap.xml',
'src/lib/**/*.js',
+ 'src/lib/**/*.html',
].map(path => eleventyConfig.addPassthroughCopy(path));
// eleventyConfig.addPassthroughCopy('src/favicon');
diff --git a/src/lib/free-queue/coi-serviceworker.js b/src/lib/free-queue/coi-serviceworker.js
new file mode 100644
index 000000000..646aadf10
--- /dev/null
+++ b/src/lib/free-queue/coi-serviceworker.js
@@ -0,0 +1,115 @@
+/*! coi-serviceworker v0.1.6 - Guido Zuidhof, licensed under MIT */
+let coepCredentialless = false;
+if (typeof window === 'undefined') {
+ self.addEventListener("install", () => self.skipWaiting());
+ self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
+
+ self.addEventListener("message", (ev) => {
+ if (!ev.data) {
+ return;
+ } else if (ev.data.type === "deregister") {
+ self.registration
+ .unregister()
+ .then(() => {
+ return self.clients.matchAll();
+ })
+ .then(clients => {
+ clients.forEach((client) => client.navigate(client.url));
+ });
+ } else if (ev.data.type === "coepCredentialless") {
+ coepCredentialless = ev.data.value;
+ }
+ });
+
+ self.addEventListener("fetch", function (event) {
+ const r = event.request;
+ if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
+ return;
+ }
+
+ const request = (coepCredentialless && r.mode === "no-cors")
+ ? new Request(r, {
+ credentials: "omit",
+ })
+ : r;
+ event.respondWith(
+ fetch(request)
+ .then((response) => {
+ if (response.status === 0) {
+ return response;
+ }
+
+ const newHeaders = new Headers(response.headers);
+ newHeaders.set("Cross-Origin-Embedder-Policy",
+ coepCredentialless ? "credentialless" : "require-corp"
+ );
+ newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
+
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: newHeaders,
+ });
+ })
+ .catch((e) => console.error(e))
+ );
+ });
+
+} else {
+ (() => {
+ // You can customize the behavior of this script through a global `coi` variable.
+ const coi = {
+ shouldRegister: () => true,
+ shouldDeregister: () => false,
+ coepCredentialless: () => false,
+ doReload: () => window.location.reload(),
+ quiet: false,
+ ...window.coi
+ };
+
+ const n = navigator;
+
+ if (n.serviceWorker && n.serviceWorker.controller) {
+ n.serviceWorker.controller.postMessage({
+ type: "coepCredentialless",
+ value: coi.coepCredentialless(),
+ });
+
+ if (coi.shouldDeregister()) {
+ n.serviceWorker.controller.postMessage({ type: "deregister" });
+ }
+ }
+
+ // If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
+ // already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
+ if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;
+
+ if (!window.isSecureContext) {
+ !coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required.");
+ return;
+ }
+
+ // In some environments (e.g. Chrome incognito mode) this won't be available
+ if (n.serviceWorker) {
+ n.serviceWorker.register(window.document.currentScript.src).then(
+ (registration) => {
+ !coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope);
+
+ registration.addEventListener("updatefound", () => {
+ !coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker.");
+ coi.doReload();
+ });
+
+ // If the registration is active, but it's not controlling the page
+ if (registration.active && !n.serviceWorker.controller) {
+ !coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker.");
+ coi.doReload();
+ }
+ },
+ (err) => {
+ !coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err);
+ }
+ );
+ }
+ })();
+}
\ No newline at end of file
diff --git a/src/lib/free-queue/free-queue.js b/src/lib/free-queue/free-queue.js
index 122d5b2bc..0ad7ade9b 100644
--- a/src/lib/free-queue/free-queue.js
+++ b/src/lib/free-queue/free-queue.js
@@ -190,6 +190,10 @@ class FreeQueue {
// The channel count of arraySequence and the length of each channel must
// match with this buffer obejct.
+ if (arraySequence.length !== this._channelCount) {
+ throw new Error(`Channel count mismatch: expected ${this._channelCount}, but got ${arraySequence.length}.`);
+ }
+
// Transfer data from the |arraySequence| storage to the internal buffer.
const sourceLength = arraySequence[0].length;
for (let i = 0; i < sourceLength; ++i) {
@@ -215,6 +219,10 @@ class FreeQueue {
// The channel count of arraySequence and the length of each channel must
// match with this buffer obejct.
+ if (arraySequence.length !== this._channelCount) {
+ throw new Error(`Channel count mismatch: expected ${this._channelCount}, but got ${arraySequence.length}.`);
+ }
+
// If the FIFO is completely empty, do nothing.
if (this._framesAvailable === 0) {
return;
diff --git a/src/lib/free-queue/test/free-queue.test.html b/src/lib/free-queue/test/free-queue.test.html
new file mode 100644
index 000000000..ae35422d6
--- /dev/null
+++ b/src/lib/free-queue/test/free-queue.test.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+ FreeQueue Tests
+
+
+
+
+
+ FreeQueue Test
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/free-queue/test/free-queue.test.js b/src/lib/free-queue/test/free-queue.test.js
new file mode 100644
index 000000000..25cbe9411
--- /dev/null
+++ b/src/lib/free-queue/test/free-queue.test.js
@@ -0,0 +1,191 @@
+import { expect } from 'https://cdnjs.cloudflare.com/ajax/libs/chai/5.1.1/chai.js';
+import { FreeQueue, MAX_CHANNEL_COUNT, RENDER_QUANTUM_FRAMES } from '../free-queue.js';
+
+// Mock WASM module
+const mockWasmModule = {
+ // Simulate memory allocation
+ _malloc: (size) => new ArrayBuffer(size),
+ // Simulate memory deallocation
+ _free: () => { },
+ // Simulate HEAPF32
+ HEAPF32: new Float32Array(1024),
+};
+
+describe('FreeQueue Class', () => {
+ const bufferLength = 1024;
+ const channelCount = 2;
+ const maxChannelCount = 4;
+ let freeQueue = null;
+
+ beforeEach(() => {
+ freeQueue = new FreeQueue(mockWasmModule, bufferLength, channelCount, maxChannelCount);
+ });
+
+ afterEach(() => {
+ freeQueue.free();
+ });
+
+ describe('Initialization', () => {
+ it('should initialize with correct properties', () => {
+ expect(freeQueue.length).to.equal(bufferLength);
+ expect(freeQueue.numberOfChannels).to.equal(channelCount);
+ expect(freeQueue.maxChannelCount).to.equal(maxChannelCount);
+ });
+
+ it('should allocate the correct amount of memory', () => {
+ const dataByteSize = channelCount * bufferLength * Float32Array.BYTES_PER_ELEMENT;
+ expect(freeQueue.getPointer()).to.be.instanceof(ArrayBuffer);
+ expect(freeQueue.getPointer().byteLength).to.equal(dataByteSize);
+ });
+ });
+
+ describe('Channel Adaptation', () => {
+ it('should adapt to a new channel count within limits', () => {
+ freeQueue.adaptChannel(3);
+ expect(freeQueue.numberOfChannels).to.equal(3);
+ });
+ it('should not adapt to a channel count exceeding maxChannelCount', () => {
+ const maxChannelCount = 8;
+ const initialChannelCount = freeQueue.numberOfChannels;
+
+ try {
+ freeQueue.adaptChannel(maxChannelCount + 1);
+ } catch (error) {
+ expect(error).to.be.instanceOf(Error);
+ expect(error.message).to.include('exceeds the maximum channel count');
+ }
+
+ expect(freeQueue.numberOfChannels).to.equal(initialChannelCount);
+ });
+ });
+
+ describe('Push Data', () => {
+ it('should correctly push data', () => {
+ const testData = [new Float32Array(bufferLength).fill(1), new Float32Array(bufferLength).fill(2)];
+ freeQueue.push(testData);
+
+ const outputData = [new Float32Array(bufferLength), new Float32Array(bufferLength)];
+ freeQueue.pull(outputData);
+
+ expect(outputData[0]).to.deep.equal(testData[0]);
+ expect(outputData[1]).to.deep.equal(testData[1]);
+ });
+
+ it('should handle buffer overflow correctly', () => {
+ const testData = [new Float32Array(bufferLength * 2).fill(1), new Float32Array(bufferLength * 2).fill(2)];
+ freeQueue.push(testData);
+
+ expect(freeQueue.framesAvailable).to.equal(bufferLength);
+ });
+
+ it('should handle multiple push cycles', () => {
+ const testData = [new Float32Array(bufferLength).fill(1), new Float32Array(bufferLength).fill(2)];
+
+ for (let i = 0; i < 5; i++) {
+ freeQueue.push(testData);
+
+ const outputData = [new Float32Array(bufferLength), new Float32Array(bufferLength)];
+ freeQueue.pull(outputData);
+
+ expect(outputData[0]).to.deep.equal(testData[0]);
+ expect(outputData[1]).to.deep.equal(testData[1]);
+ expect(freeQueue.framesAvailable).to.equal(0);
+ }
+ });
+ });
+
+ describe('Pull Data', () => {
+ it('should correctly pull data', () => {
+ const testData = [new Float32Array(bufferLength).fill(1), new Float32Array(bufferLength).fill(2)];
+ freeQueue.push(testData);
+
+ const outputData = [new Float32Array(bufferLength), new Float32Array(bufferLength)];
+ freeQueue.pull(outputData);
+
+ expect(outputData[0]).to.deep.equal(testData[0]);
+ expect(outputData[1]).to.deep.equal(testData[1]);
+ });
+
+ it('should not pull data when buffer is empty', () => {
+ const outputData = [new Float32Array(bufferLength), new Float32Array(bufferLength)];
+ freeQueue.pull(outputData);
+
+ expect(outputData[0]).to.deep.equal(new Float32Array(bufferLength));
+ expect(outputData[1]).to.deep.equal(new Float32Array(bufferLength));
+ });
+
+ it('should manage partial data pulls', () => {
+ const testData = [new Float32Array(bufferLength).fill(1), new Float32Array(bufferLength).fill(2)];
+ freeQueue.push(testData);
+
+ const partialOutput = [new Float32Array(bufferLength / 2), new Float32Array(bufferLength / 2)];
+ freeQueue.pull(partialOutput);
+
+ expect(partialOutput[0]).to.deep.equal(new Float32Array(bufferLength / 2).fill(1));
+ expect(partialOutput[1]).to.deep.equal(new Float32Array(bufferLength / 2).fill(2));
+ expect(freeQueue.framesAvailable).to.equal(bufferLength / 2);
+ });
+
+ it('should handle multiple pull cycles', () => {
+ const testData = [new Float32Array(bufferLength).fill(1), new Float32Array(bufferLength).fill(2)];
+
+ for (let i = 0; i < 5; i++) {
+ freeQueue.push(testData);
+
+ const outputData = [new Float32Array(bufferLength), new Float32Array(bufferLength)];
+ freeQueue.pull(outputData);
+
+ expect(outputData[0]).to.deep.equal(testData[0]);
+ expect(outputData[1]).to.deep.equal(testData[1]);
+ expect(freeQueue.framesAvailable).to.equal(0);
+ }
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('should return null for invalid channel index in getChannelData', () => {
+ const invalidIndex = channelCount + 1;
+ expect(freeQueue.getChannelData(invalidIndex)).to.be.null;
+ });
+
+ it('should throw an error if pushing with mismatched channel count', () => {
+ const invalidTestData = [new Float32Array(bufferLength).fill(1)];
+
+ const expectedChannelCount = freeQueue._channelCount;
+ const actualChannelCount = invalidTestData.length;
+
+ expect(() => freeQueue.push(invalidTestData))
+ .to.throw(Error, `Channel count mismatch: expected ${expectedChannelCount}, but got ${actualChannelCount}.`);
+ });
+
+ it('should throw an error if pulling with mismatched channel count', () => {
+ const invalidOutputData = [new Float32Array(bufferLength)];
+
+ const expectedChannelCount = freeQueue._channelCount;
+ const actualChannelCount = invalidOutputData.length;
+
+ expect(() => freeQueue.pull(invalidOutputData))
+ .to.throw(Error, `Channel count mismatch: expected ${expectedChannelCount}, but got ${actualChannelCount}.`);
+ });
+ });
+
+ describe('Performance Tests', function () {
+ it('should handle large data efficiently', function () {
+ const input = [new Float32Array(bufferLength), new Float32Array(bufferLength)];
+ const output = [new Float32Array(bufferLength), new Float32Array(bufferLength)];
+ for (let i = 0; i < 10000; i++) {
+ freeQueue.push(input, bufferLength);
+ freeQueue.pull(output, bufferLength);
+ }
+ });
+
+ it('should handle small data efficiently', function () {
+ const input = [new Float32Array(1), new Float32Array(1)];
+ const output = [new Float32Array(1), new Float32Array(1)];
+ for (let i = 0; i < 1000000; i++) {
+ freeQueue.push(input, 1);
+ freeQueue.pull(output, 1);
+ }
+ });
+ });
+});