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); + } + }); + }); +});