diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml
index 086fae4..6104535 100644
--- a/.github/workflows/actions.yml
+++ b/.github/workflows/actions.yml
@@ -16,6 +16,11 @@ jobs:
- run: npm ci && npm run build
working-directory: src/ds-playground
- run: cp -r src/ds-playground/dist out/ds-playground
+ - name: Build AECDump Viewer
+ run: yarn install --frozen-lockfile --registry https://registry.npmjs.org && yarn run build
+ working-directory: src/aecdump-viewer
+ - name: Copy AECDump Viewer to out
+ run: cp -r src/aecdump-viewer/dist out/aecdump-viewer
- uses: actions/upload-artifact@v4
with:
name: out
diff --git a/src/aecdump-viewer/.editorconfig b/src/aecdump-viewer/.editorconfig
new file mode 100644
index 0000000..c8c2d2a
--- /dev/null
+++ b/src/aecdump-viewer/.editorconfig
@@ -0,0 +1,29 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+
+[*]
+
+# Change these settings to your own preference
+indent_style = space
+indent_size = 2
+
+# We recommend you to keep these unchanged
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.json]
+indent_size = 2
+
+[*.{html,js,md}]
+block_comment_start = /**
+block_comment = *
+block_comment_end = */
diff --git a/src/aecdump-viewer/.eslintrc.json b/src/aecdump-viewer/.eslintrc.json
new file mode 100644
index 0000000..9b36e27
--- /dev/null
+++ b/src/aecdump-viewer/.eslintrc.json
@@ -0,0 +1,40 @@
+{
+ "root": true,
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": 2020,
+ "sourceType": "module"
+ },
+ "plugins": ["@typescript-eslint"],
+ "env": {
+ "browser": true
+ },
+ "rules": {
+ "no-prototype-builtins": "off",
+ "@typescript-eslint/ban-types": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off",
+ "@typescript-eslint/no-explicit-any": "error",
+ "@typescript-eslint/no-empty-function": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ "argsIgnorePattern": "^_"
+ }
+ ]
+ },
+ "overrides": [
+ {
+ "files": ["rollup.config.js", "web-dev-server.config.mjs"],
+ "env": {
+ "node": true
+ }
+ }
+ ]
+}
diff --git a/src/aecdump-viewer/.gitignore b/src/aecdump-viewer/.gitignore
new file mode 100644
index 0000000..48de0cd
--- /dev/null
+++ b/src/aecdump-viewer/.gitignore
@@ -0,0 +1,34 @@
+# Dependencies
+node_modules/
+.pnp
+.pnp.js
+
+# Production builds
+dist/
+out-tsc/
+
+# Generated Protobuf files
+src/proto/debug.js
+src/proto/debug.d.ts
+
+# Playwright test artifacts
+test-results/
+playwright-report/
+blob-report/
+playwright/.cache/
+
+# Editor configs and OS files
+.DS_Store
+*.local
+.env
+.env.local
+.env.*.local
+
+# IDEs
+.idea/
+.vscode/
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.swp
diff --git a/src/aecdump-viewer/index.html b/src/aecdump-viewer/index.html
new file mode 100644
index 0000000..ad1dc5b
--- /dev/null
+++ b/src/aecdump-viewer/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+ AECDump Web Viewer
+
+
+
+
+
+
+
+
+
diff --git a/src/aecdump-viewer/package.json b/src/aecdump-viewer/package.json
new file mode 100644
index 0000000..aac8477
--- /dev/null
+++ b/src/aecdump-viewer/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "aecdump-viewer",
+ "description": "Web-based AECDump Viewer",
+ "license": "Apache-2.0",
+ "author": "aaronyu",
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "build": "yarn run proto:generate && tsc && vite build",
+ "start": "yarn run proto:generate && vite",
+ "serve:dist": "vite preview --port 8080",
+ "serve:dev": "vite --port 8000",
+ "test": "playwright test",
+ "test:dev": "TEST_ENV=dev playwright test",
+ "proto:generate": "pbjs -t static-module -w es6 -o src/proto/debug.js src/proto/debug.proto && pbts -o src/proto/debug.d.ts src/proto/debug.js"
+ },
+ "dependencies": {
+ "lit": "^3.1.1",
+ "protobufjs": "^7.2.4",
+ "wavesurfer.js": "^7.7.15"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.41.2",
+ "protobufjs-cli": "^1.1.1",
+ "tslib": "^2.6.2",
+ "typescript": "^5.3.3",
+ "vite": "^5.0.12"
+ }
+}
diff --git a/src/aecdump-viewer/playwright.config.ts b/src/aecdump-viewer/playwright.config.ts
new file mode 100644
index 0000000..34fdbe9
--- /dev/null
+++ b/src/aecdump-viewer/playwright.config.ts
@@ -0,0 +1,35 @@
+import { defineConfig, devices } from '@playwright/test';
+
+const isDev = process.env.TEST_ENV === 'dev';
+const port = isDev ? 8000 : 8080;
+const command = isDev ? 'yarn run serve:dev' : 'yarn run serve:dist';
+
+export default defineConfig({
+ testDir: './tests',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: 'list',
+ use: {
+ baseURL: `http://localhost:${port}`,
+ trace: 'on-first-retry',
+ screenshot: 'only-on-failure',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+
+ /* Run local dev server before starting tests */
+ webServer: {
+ command: command,
+ url: `http://localhost:${port}`,
+ reuseExistingServer: !process.env.CI,
+ timeout: 20 * 1000, // Dev server compilation might take slightly longer
+ },
+});
diff --git a/src/aecdump-viewer/src/aecdump-viewer.ts b/src/aecdump-viewer/src/aecdump-viewer.ts
new file mode 100644
index 0000000..fccf6f0
--- /dev/null
+++ b/src/aecdump-viewer/src/aecdump-viewer.ts
@@ -0,0 +1,567 @@
+import { LitElement, html, css } from 'lit';
+import { customElement, state } from 'lit/decorators.js';
+import WaveSurfer from 'wavesurfer.js';
+import { parseAecDump, DecoderResult } from './decoder.js';
+import { audioBufferToWav } from './wav-helper.js';
+
+interface Track {
+ id: 'ref' | 'mic' | 'out';
+ name: string;
+ ws: WaveSurfer | null;
+ muted: boolean;
+ soloed: boolean;
+ volume: number;
+ url: string | null;
+}
+
+@customElement('aecdump-viewer')
+export class AecDumpViewer extends LitElement {
+ @state() private loading = false;
+ @state() private loadingStatus = '';
+ @state() private isPlaying = false;
+ @state() private duration = 0;
+ @state() private currentTime = 0;
+
+ @state() private tracks: Record = {
+ ref: { id: 'ref', name: 'Reference (Playout)', ws: null, muted: false, soloed: false, volume: 1.0, url: null },
+ mic: { id: 'mic', name: 'Microphone Input', ws: null, muted: false, soloed: false, volume: 1.0, url: null },
+ out: { id: 'out', name: 'Processed Output', ws: null, muted: false, soloed: false, volume: 1.0, url: null },
+ };
+
+ private audioCtx: AudioContext | null = null;
+ private syncSeeking = false;
+
+ static override styles = css`
+ :host {
+ display: block;
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ color: #333;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+ }
+
+ header {
+ margin-bottom: 30px;
+ border-bottom: 1px solid #eee;
+ padding-bottom: 20px;
+ }
+
+ h1 {
+ margin: 0 0 10px 0;
+ font-size: 24px;
+ color: #1a73e8;
+ }
+
+ .description {
+ margin: 0;
+ color: #666;
+ font-size: 14px;
+ }
+
+ .dropzone {
+ border: 2px dashed #ccc;
+ border-radius: 8px;
+ padding: 40px 20px;
+ text-align: center;
+ background: #fafafa;
+ cursor: pointer;
+ transition: border-color 0.2s, background-color 0.2s;
+ margin-bottom: 20px;
+ }
+
+ .dropzone:hover, .dropzone.dragover {
+ border-color: #1a73e8;
+ background: #f1f3f4;
+ }
+
+ .dropzone p {
+ margin: 0;
+ font-size: 16px;
+ color: #5f6368;
+ }
+
+ .dropzone input {
+ display: none;
+ }
+
+ .status {
+ padding: 10px 15px;
+ border-radius: 4px;
+ background: #e8f0fe;
+ color: #1a73e8;
+ margin-bottom: 20px;
+ font-size: 14px;
+ }
+
+ .controls {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ margin-bottom: 25px;
+ background: #f8f9fa;
+ padding: 15px;
+ border-radius: 8px;
+ border: 1px solid #e0e0e0;
+ }
+
+ button {
+ background: #1a73e8;
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.2s;
+ font-size: 14px;
+ }
+
+ button:hover {
+ background: #1557b0;
+ }
+
+ button:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+ }
+
+ button.secondary {
+ background: #f1f3f4;
+ color: #3c4043;
+ border: 1px solid #dadce0;
+ }
+
+ button.secondary:hover {
+ background: #e8eaed;
+ }
+
+ button.active {
+ background: #d93025;
+ }
+
+ button.active:hover {
+ background: #b0251a;
+ }
+
+ .time-display {
+ font-family: monospace;
+ font-size: 14px;
+ color: #5f6368;
+ margin-left: auto;
+ }
+
+ .tracks-container {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ }
+
+ .track-card {
+ border: 1px solid #dadce0;
+ border-radius: 8px;
+ background: white;
+ overflow: hidden;
+ box-shadow: 0 1px 2px 0 rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15);
+ }
+
+ .track-header {
+ background: #f8f9fa;
+ padding: 10px 15px;
+ border-bottom: 1px solid #dadce0;
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ }
+
+ .track-title {
+ font-weight: 600;
+ font-size: 14px;
+ color: #3c4043;
+ }
+
+ .track-controls {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-left: auto;
+ }
+
+ .track-controls button {
+ padding: 4px 8px;
+ font-size: 12px;
+ }
+
+ .track-controls button.mute.active {
+ background: #f28b82;
+ color: #b00020;
+ border-color: #f28b82;
+ }
+
+ .track-controls button.solo.active {
+ background: #fdd663;
+ color: #875900;
+ border-color: #fdd663;
+ }
+
+ .volume-slider {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 12px;
+ color: #5f6368;
+ }
+
+ .volume-slider input {
+ width: 80px;
+ }
+
+ .track-body {
+ padding: 15px;
+ background: #fafafa;
+ position: relative;
+ }
+
+ .waveform-container {
+ background: white;
+ border: 1px solid #eee;
+ border-radius: 4px;
+ min-height: 80px;
+ }
+ `;
+
+ override render() {
+ const hasTracks = Object.values(this.tracks).some(t => t.url !== null);
+
+ return html`
+
+
+
+
${this.loading ? 'Parsing dump...' : 'Drag & drop an aecdump/protobuf file here, or click to select'}
+
+
+
+ ${this.loadingStatus ? html`${this.loadingStatus}
` : ''}
+
+ ${hasTracks ? html`
+
+
+
+
+
+ ${this.formatTime(this.currentTime)} / ${this.formatTime(this.duration)}
+
+
+
+
+ ${Object.values(this.tracks).map(track => this.renderTrackCard(track))}
+
+ ` : ''}
+ `;
+ }
+
+ private renderTrackCard(track: Track) {
+ if (!track.url) return '';
+
+ return html`
+
+ `;
+ }
+
+ // File selection & drag-drop handling
+ private triggerFileSelect() {
+ this.shadowRoot?.getElementById('fileInput')?.click();
+ }
+
+ private onDragOver(e: DragEvent) {
+ e.preventDefault();
+ this.shadowRoot?.querySelector('.dropzone')?.classList.add('dragover');
+ }
+
+ private onDragLeave() {
+ this.shadowRoot?.querySelector('.dropzone')?.classList.remove('dragover');
+ }
+
+ private onDrop(e: DragEvent) {
+ e.preventDefault();
+ this.onDragLeave();
+ const file = e.dataTransfer?.files[0];
+ if (file) this.processFile(file);
+ }
+
+ private onFileSelected(e: Event) {
+ const file = (e.target as HTMLInputElement).files?.[0];
+ if (file) this.processFile(file);
+ }
+
+ private async processFile(file: File) {
+ this.loading = true;
+ this.loadingStatus = `Loading file: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)...`;
+ this.stopAll();
+ this.destroyWaveSurfers();
+
+ try {
+ const arrayBuffer = await file.arrayBuffer();
+ this.loadingStatus = 'Parsing AECDump protobuf data...';
+
+ // Small delay to allow UI to update
+ await new Promise(resolve => setTimeout(resolve, 50));
+
+ const parsed = parseAecDump(arrayBuffer);
+
+ this.loadingStatus = 'Decoding audio and preparing tracks...';
+ await new Promise(resolve => setTimeout(resolve, 50));
+
+ await this.initializeTracks(parsed);
+ this.loadingStatus = 'AECDump loaded successfully!';
+ } catch (error) {
+ console.error(error);
+ this.loadingStatus = `Error: ${(error as Error).message}`;
+ } finally {
+ this.loading = false;
+ }
+ }
+
+ // Convert parsed raw PCM streams into WAV Blob URLs
+ private async initializeTracks(parsed: DecoderResult) {
+ if (!this.audioCtx) {
+ this.audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
+ }
+
+ // Helper to convert ParsedAudioStream to Blob URL
+ const createWavUrl = (stream: typeof parsed.reference) => {
+ if (stream.channelData.length === 0 || stream.channelData[0].length === 0) {
+ return null;
+ }
+ // Create AudioBuffer
+ const buffer = this.audioCtx!.createBuffer(
+ stream.channels,
+ stream.channelData[0].length,
+ stream.sampleRate
+ );
+ for (let c = 0; c < stream.channels; c++) {
+ buffer.copyToChannel(stream.channelData[c] as any, c);
+ }
+ // Encode to WAV
+ const wavBytes = audioBufferToWav(buffer);
+ const blob = new Blob([wavBytes], { type: 'audio/wav' });
+ return URL.createObjectURL(blob);
+ };
+
+ // Generate URLs
+ const refUrl = createWavUrl(parsed.reference);
+ const micUrl = createWavUrl(parsed.input);
+ const outUrl = createWavUrl(parsed.output);
+
+ // Update track state
+ this.tracks = {
+ ref: { ...this.tracks.ref, url: refUrl },
+ mic: { ...this.tracks.mic, url: micUrl },
+ out: { ...this.tracks.out, url: outUrl },
+ };
+
+ // Request Lit update so track-cards render, then initialize WaveSurfers on the DOM
+ this.requestUpdate();
+ await this.updateComplete;
+
+ this.initWaveSurfers();
+ }
+
+ private initWaveSurfers() {
+ const wsOptions = {
+ height: 80,
+ waveColor: '#a8c7fa',
+ progressColor: '#1a73e8',
+ cursorColor: '#3c4043',
+ cursorWidth: 2,
+ dragToSeek: true,
+ normalize: true,
+ };
+
+ // Initialize each active track
+ Object.values(this.tracks).forEach(track => {
+ if (!track.url) return;
+
+ const container = this.shadowRoot?.getElementById(`waveform-${track.id}`);
+ if (!container) return;
+
+ const ws = WaveSurfer.create({
+ ...wsOptions,
+ container: container,
+ url: track.url,
+ });
+
+ track.ws = ws;
+
+ // Sync Mute/Volume state
+ ws.setMuted(track.muted);
+ ws.setVolume(track.volume);
+
+ // Bind events
+ if (track.id === 'mic') {
+ // Use mic as master track for duration/currentTime state updates
+ ws.on('ready', (duration) => {
+ this.duration = duration;
+ });
+ ws.on('timeupdate', (time) => {
+ this.currentTime = time;
+ });
+ ws.on('finish', () => {
+ this.isPlaying = false;
+ });
+ }
+
+ // Synchronized Seeking
+ ws.on('interaction', () => {
+ if (this.syncSeeking) return;
+ this.syncSeeking = true;
+
+ const time = ws.getCurrentTime();
+ Object.values(this.tracks).forEach(t => {
+ if (t.id !== track.id && t.ws) {
+ t.ws.setTime(time);
+ }
+ });
+
+ this.syncSeeking = false;
+ });
+ });
+ }
+
+ private destroyWaveSurfers() {
+ Object.values(this.tracks).forEach(track => {
+ if (track.ws) {
+ track.ws.destroy();
+ track.ws = null;
+ }
+ if (track.url) {
+ URL.revokeObjectURL(track.url);
+ track.url = null;
+ }
+ });
+ this.isPlaying = false;
+ this.duration = 0;
+ this.currentTime = 0;
+ }
+
+ // Master controls
+ private togglePlay() {
+ const activeWs = Object.values(this.tracks).map(t => t.ws).filter(Boolean) as WaveSurfer[];
+ if (activeWs.length === 0) return;
+
+ if (this.isPlaying) {
+ activeWs.forEach(ws => ws.pause());
+ this.isPlaying = false;
+ } else {
+ // Play all
+ activeWs.forEach(ws => ws.play());
+ this.isPlaying = true;
+ }
+ }
+
+ private stopAll() {
+ Object.values(this.tracks).forEach(t => {
+ if (t.ws) {
+ t.ws.stop();
+ }
+ });
+ this.isPlaying = false;
+ this.currentTime = 0;
+ }
+
+ // Track controls
+ private onVolumeChange(id: 'ref' | 'mic' | 'out', e: Event) {
+ const value = parseFloat((e.target as HTMLInputElement).value);
+ const track = this.tracks[id];
+ track.volume = value;
+ if (track.ws) {
+ track.ws.setVolume(value);
+ }
+ this.requestUpdate();
+ }
+
+ private toggleMute(id: 'ref' | 'mic' | 'out') {
+ const track = this.tracks[id];
+ track.muted = !track.muted;
+ if (track.ws) {
+ track.ws.setMuted(track.muted);
+ }
+ this.requestUpdate();
+ }
+
+ private toggleSolo(id: 'ref' | 'mic' | 'out') {
+ const track = this.tracks[id];
+ track.soloed = !track.soloed;
+
+ const hasSoloedTracks = Object.values(this.tracks).some(t => t.soloed);
+
+ Object.values(this.tracks).forEach(t => {
+ if (!t.ws) return;
+
+ if (hasSoloedTracks) {
+ // If there are soloed tracks, mute this track unless it is soloed
+ t.ws.setMuted(!t.soloed);
+ } else {
+ // Otherwise restore the track's own mute state
+ t.ws.setMuted(t.muted);
+ }
+ });
+
+ this.requestUpdate();
+ }
+
+ private formatTime(seconds: number): string {
+ const min = Math.floor(seconds / 60);
+ const sec = Math.floor(seconds % 60);
+ const ms = Math.floor((seconds % 1) * 100);
+ return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}`;
+ }
+
+ override disconnectedCallback() {
+ super.disconnectedCallback();
+ this.destroyWaveSurfers();
+ }
+}
diff --git a/src/aecdump-viewer/src/decoder.ts b/src/aecdump-viewer/src/decoder.ts
new file mode 100644
index 0000000..cf88b94
--- /dev/null
+++ b/src/aecdump-viewer/src/decoder.ts
@@ -0,0 +1,206 @@
+import { webrtc } from './proto/debug.js';
+
+const Event = webrtc.audioproc.Event;
+
+export interface ParsedAudioStream {
+ sampleRate: number;
+ channels: number;
+ channelData: Float32Array[];
+}
+
+export interface DecoderResult {
+ reference: ParsedAudioStream;
+ input: ParsedAudioStream;
+ output: ParsedAudioStream;
+}
+
+class ChannelAccumulator {
+ private chunks: Float32Array[] = [];
+ private totalLength = 0;
+
+ append(chunk: Float32Array) {
+ this.chunks.push(chunk);
+ this.totalLength += chunk.length;
+ }
+
+ get length() {
+ return this.totalLength;
+ }
+
+ getMerged(): Float32Array {
+ const merged = new Float32Array(this.totalLength);
+ let offset = 0;
+ for (const chunk of this.chunks) {
+ merged.set(chunk, offset);
+ offset += chunk.length;
+ }
+ return merged;
+ }
+}
+
+class StreamAccumulator {
+ public accumulators: ChannelAccumulator[] = [];
+ public sampleRate = 0;
+ public channels = 0;
+
+ init(sampleRate: number, channels: number) {
+ if (this.sampleRate === 0) {
+ this.sampleRate = sampleRate;
+ this.channels = channels;
+ this.accumulators = Array.from({ length: channels }, () => new ChannelAccumulator());
+ } else if (this.sampleRate !== sampleRate || this.channels !== channels) {
+ console.warn(
+ `StreamAccumulator: Audio format changed mid-dump! ` +
+ `Old: ${this.sampleRate}Hz/${this.channels}ch, ` +
+ `New: ${sampleRate}Hz/${channels}ch. V1 ignores mid-dump format changes.`
+ );
+ }
+ }
+
+ appendInterleavedInt16(bytes: Uint8Array) {
+ if (this.channels === 0) return;
+ const int16 = new Int16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 2);
+ const numSamples = int16.length / this.channels;
+
+ // Temporary chunks for each channel
+ const chunks = Array.from({ length: this.channels }, () => new Float32Array(numSamples));
+
+ let index = 0;
+ for (let i = 0; i < numSamples; i++) {
+ for (let c = 0; c < this.channels; c++) {
+ chunks[c][i] = int16[index++] / 32768.0;
+ }
+ }
+
+ for (let c = 0; c < this.channels; c++) {
+ this.accumulators[c].append(chunks[c]);
+ }
+ }
+
+ appendDeinterleavedFloat(channelsBytes: Uint8Array[]) {
+ if (this.channels === 0) return;
+ const actualChannels = Math.min(this.channels, channelsBytes.length);
+ for (let c = 0; c < actualChannels; c++) {
+ const bytes = channelsBytes[c];
+ let floatData: Float32Array;
+ if (bytes.byteOffset % 4 === 0) {
+ floatData = new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4);
+ } else {
+ const copy = new Uint8Array(bytes.byteLength);
+ copy.set(bytes);
+ floatData = new Float32Array(copy.buffer, 0, copy.byteLength / 4);
+ }
+ this.accumulators[c].append(floatData);
+ }
+ }
+
+ toParsedStream(): ParsedAudioStream {
+ return {
+ sampleRate: this.sampleRate || 16000, // fallback
+ channels: this.channels || 1,
+ channelData: this.accumulators.map((acc) => acc.getMerged()),
+ };
+ }
+}
+
+export function parseAecDump(arrayBuffer: ArrayBuffer): DecoderResult {
+ const view = new DataView(arrayBuffer);
+ let offset = 0;
+
+ const refAcc = new StreamAccumulator();
+ const inputAcc = new StreamAccumulator();
+ const outputAcc = new StreamAccumulator();
+
+ let eventCount = 0;
+
+ while (offset < arrayBuffer.byteLength) {
+ if (offset + 4 > arrayBuffer.byteLength) {
+ console.warn('parseAecDump: Unexpected EOF while reading message size.');
+ break;
+ }
+ const size = view.getInt32(offset, true);
+ offset += 4;
+
+ if (offset + size > arrayBuffer.byteLength) {
+ console.warn('parseAecDump: Unexpected EOF while reading message payload.');
+ break;
+ }
+
+ const eventBytes = new Uint8Array(arrayBuffer, offset, size);
+ offset += size;
+
+ let event: webrtc.audioproc.Event;
+ try {
+ event = Event.decode(eventBytes);
+ } catch (e) {
+ console.error(`parseAecDump: Failed to decode event #${eventCount} at offset ${offset - size - 4}:`, e);
+ continue;
+ }
+
+ eventCount++;
+
+ switch (event.type) {
+ case Event.Type.INIT: {
+ const init = event.init;
+ if (!init) break;
+
+ const sampleRate = init.sampleRate || 16000;
+ const reverseSampleRate = init.reverseSampleRate || sampleRate;
+ const outputSampleRate = init.outputSampleRate || sampleRate;
+
+ const inputChannels = init.numInputChannels || 1;
+ const outputChannels = init.numOutputChannels || 1;
+ const reverseChannels = init.numReverseChannels || 1;
+
+ refAcc.init(reverseSampleRate, reverseChannels);
+ inputAcc.init(sampleRate, inputChannels);
+ outputAcc.init(outputSampleRate, outputChannels);
+ break;
+ }
+
+ case Event.Type.REVERSE_STREAM: {
+ const rev = event.reverseStream;
+ if (!rev) break;
+
+ if (rev.data && rev.data.length > 0) {
+ refAcc.appendInterleavedInt16(rev.data);
+ } else if (rev.channel && rev.channel.length > 0) {
+ refAcc.appendDeinterleavedFloat(rev.channel);
+ }
+ break;
+ }
+
+ case Event.Type.STREAM: {
+ const stream = event.stream;
+ if (!stream) break;
+
+ // Input
+ if (stream.inputData && stream.inputData.length > 0) {
+ inputAcc.appendInterleavedInt16(stream.inputData);
+ } else if (stream.inputChannel && stream.inputChannel.length > 0) {
+ inputAcc.appendDeinterleavedFloat(stream.inputChannel);
+ }
+
+ // Output
+ if (stream.outputData && stream.outputData.length > 0) {
+ outputAcc.appendInterleavedInt16(stream.outputData);
+ } else if (stream.outputChannel && stream.outputChannel.length > 0) {
+ outputAcc.appendDeinterleavedFloat(stream.outputChannel);
+ }
+ break;
+ }
+
+ default:
+ // Config, RuntimeSetting, Unknown are ignored in V1 decoder
+ break;
+ }
+ }
+
+ console.log(`parseAecDump: Successfully parsed ${eventCount} events.`);
+
+ return {
+ reference: refAcc.toParsedStream(),
+ input: inputAcc.toParsedStream(),
+ output: outputAcc.toParsedStream(),
+ };
+}
diff --git a/src/aecdump-viewer/src/proto/debug.proto b/src/aecdump-viewer/src/proto/debug.proto
new file mode 100644
index 0000000..cc5efbc
--- /dev/null
+++ b/src/aecdump-viewer/src/proto/debug.proto
@@ -0,0 +1,115 @@
+syntax = "proto2";
+option optimize_for = LITE_RUNTIME;
+package webrtc.audioproc;
+
+// Contains the format of input/output/reverse audio. An Init message is added
+// when any of the fields are changed.
+message Init {
+ optional int32 sample_rate = 1;
+ optional int32 device_sample_rate = 2 [deprecated=true];
+ optional int32 num_input_channels = 3;
+ optional int32 num_output_channels = 4;
+ optional int32 num_reverse_channels = 5;
+ optional int32 reverse_sample_rate = 6;
+ optional int32 output_sample_rate = 7;
+ optional int32 reverse_output_sample_rate = 8;
+ optional int32 num_reverse_output_channels = 9;
+ optional int64 timestamp_ms = 10;
+}
+
+// May contain interleaved or deinterleaved data, but don't store both formats.
+message ReverseStream {
+ // int16 interleaved data.
+ optional bytes data = 1;
+
+ // float deinterleaved data, where each repeated element points to a single
+ // channel buffer of data.
+ repeated bytes channel = 2;
+}
+
+// May contain interleaved or deinterleaved data, but don't store both formats.
+message Stream {
+ // int16 interleaved data.
+ optional bytes input_data = 1;
+ optional bytes output_data = 2;
+
+ optional int32 delay = 3;
+ optional sint32 drift = 4;
+ optional int32 applied_input_volume = 5;
+ optional bool keypress = 6;
+
+ // float deinterleaved data, where each repeated element points to a single
+ // channel buffer of data.
+ repeated bytes input_channel = 7;
+ repeated bytes output_channel = 8;
+}
+
+// Contains the configurations of various APM component. A Config message is
+// added when any of the fields are changed.
+message Config {
+ // Acoustic echo canceler.
+ optional bool aec_enabled = 1;
+ optional bool aec_delay_agnostic_enabled = 2;
+ optional bool aec_drift_compensation_enabled = 3;
+ optional bool aec_extended_filter_enabled = 4;
+ optional int32 aec_suppression_level = 5;
+ // Mobile AEC.
+ optional bool aecm_enabled = 6;
+ optional bool aecm_comfort_noise_enabled = 7 [deprecated = true];
+ optional int32 aecm_routing_mode = 8 [deprecated = true];
+ // Automatic gain controller.
+ optional bool agc_enabled = 9;
+ optional int32 agc_mode = 10;
+ optional bool agc_limiter_enabled = 11;
+ optional bool noise_robust_agc_enabled = 12;
+ // High pass filter.
+ optional bool hpf_enabled = 13;
+ // Noise suppression.
+ optional bool ns_enabled = 14;
+ optional int32 ns_level = 15;
+ // Transient suppression.
+ optional bool transient_suppression_enabled = 16;
+ // Semicolon-separated string containing experimental feature
+ // descriptions.
+ optional string experiments_description = 17;
+ reserved 18; // Intelligibility enhancer enabled (deprecated).
+ // Pre amplifier.
+ optional bool pre_amplifier_enabled = 19;
+ optional float pre_amplifier_fixed_gain_factor = 20;
+
+ // Next field number 21.
+}
+
+message PlayoutAudioDeviceInfo {
+ optional int32 id = 1;
+ optional int32 max_volume = 2;
+}
+
+message RuntimeSetting {
+ optional float capture_pre_gain = 1;
+ optional float custom_render_processing_setting = 2;
+ optional float capture_fixed_post_gain = 3;
+ optional int32 playout_volume_change = 4;
+ optional PlayoutAudioDeviceInfo playout_audio_device_change = 5;
+ optional bool capture_output_used = 6;
+ optional float capture_post_gain = 7;
+}
+
+message Event {
+ enum Type {
+ INIT = 0;
+ REVERSE_STREAM = 1;
+ STREAM = 2;
+ CONFIG = 3;
+ UNKNOWN_EVENT = 4;
+ RUNTIME_SETTING = 5;
+ }
+
+ required Type type = 1;
+
+ optional Init init = 2;
+ optional ReverseStream reverse_stream = 3;
+ optional Stream stream = 4;
+ optional Config config = 5;
+ optional RuntimeSetting runtime_setting = 6;
+}
diff --git a/src/aecdump-viewer/src/wav-helper.ts b/src/aecdump-viewer/src/wav-helper.ts
new file mode 100644
index 0000000..41d8980
--- /dev/null
+++ b/src/aecdump-viewer/src/wav-helper.ts
@@ -0,0 +1,110 @@
+/**
+ * Converts an AudioBuffer to a WAV file format ArrayBuffer.
+ * Adapted from pwa-audio-recorder/wav-utils.mjs
+ */
+export function audioBufferToWav(buffer: AudioBuffer, opt?: { float32?: boolean }): ArrayBuffer {
+ opt = opt || {};
+ const numChannels = buffer.numberOfChannels;
+ const sampleRate = buffer.sampleRate;
+ const format = opt.float32 ? 3 : 1; // 3 = IEEE Float, 1 = LPCM
+ const bitDepth = format === 3 ? 32 : 16;
+
+ let result: Float32Array;
+ if (numChannels === 2) {
+ result = interleave(buffer.getChannelData(0), buffer.getChannelData(1));
+ } else if (numChannels > 2) {
+ // For V1, if more than 2 channels, we just take the first one, or we could interleave all.
+ // Let's take the first one for simplicity, or we can log a warning.
+ console.warn(`audioBufferToWav: ${numChannels} channels detected. Downmixing to mono (first channel) for V1.`);
+ result = buffer.getChannelData(0);
+ } else {
+ result = buffer.getChannelData(0);
+ }
+
+ return encodeWAV(result, format, sampleRate, numChannels, bitDepth);
+}
+
+function encodeWAV(
+ samples: Float32Array,
+ format: number,
+ sampleRate: number,
+ numChannels: number,
+ bitDepth: number
+): ArrayBuffer {
+ const bytesPerSample = bitDepth / 8;
+ const blockAlign = numChannels * bytesPerSample;
+
+ const buffer = new ArrayBuffer(44 + samples.length * bytesPerSample);
+ const view = new DataView(buffer);
+
+ /* RIFF identifier */
+ writeString(view, 0, 'RIFF');
+ /* RIFF chunk length */
+ view.setUint32(4, 36 + samples.length * bytesPerSample, true);
+ /* RIFF type */
+ writeString(view, 8, 'WAVE');
+ /* format chunk identifier */
+ writeString(view, 12, 'fmt ');
+ /* format chunk length */
+ view.setUint32(16, 16, true);
+ /* sample format (raw) */
+ view.setUint16(20, format, true);
+ /* channel count */
+ view.setUint16(22, numChannels, true);
+ /* sample rate */
+ view.setUint32(24, sampleRate, true);
+ /* byte rate (sample rate * block align) */
+ view.setUint32(28, sampleRate * blockAlign, true);
+ /* block align (channel count * bytes per sample) */
+ view.setUint16(32, blockAlign, true);
+ /* bits per sample */
+ view.setUint16(34, bitDepth, true);
+ /* data chunk identifier */
+ writeString(view, 36, 'data');
+ /* data chunk length */
+ view.setUint32(40, samples.length * bytesPerSample, true);
+
+ if (format === 1) {
+ // PCM 16-bit
+ floatTo16BitPCM(view, 44, samples);
+ } else {
+ // Float 32-bit
+ writeFloat32(view, 44, samples);
+ }
+
+ return buffer;
+}
+
+function interleave(inputL: Float32Array, inputR: Float32Array): Float32Array {
+ const length = inputL.length + inputR.length;
+ const result = new Float32Array(length);
+
+ let index = 0;
+ let inputIndex = 0;
+
+ while (index < length) {
+ result[index++] = inputL[inputIndex];
+ result[index++] = inputR[inputIndex];
+ inputIndex++;
+ }
+ return result;
+}
+
+function writeString(view: DataView, offset: number, string: string) {
+ for (let i = 0; i < string.length; i++) {
+ view.setUint8(offset + i, string.charCodeAt(i));
+ }
+}
+
+function floatTo16BitPCM(output: DataView, offset: number, input: Float32Array) {
+ for (let i = 0; i < input.length; i++, offset += 2) {
+ const s = Math.max(-1, Math.min(1, input[i]));
+ output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
+ }
+}
+
+function writeFloat32(output: DataView, offset: number, input: Float32Array) {
+ for (let i = 0; i < input.length; i++, offset += 4) {
+ output.setFloat32(offset, input[i], true);
+ }
+}
diff --git a/src/aecdump-viewer/tests/page-load.spec.ts b/src/aecdump-viewer/tests/page-load.spec.ts
new file mode 100644
index 0000000..4a1e802
--- /dev/null
+++ b/src/aecdump-viewer/tests/page-load.spec.ts
@@ -0,0 +1,69 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('AECDump Web Viewer Page Load', () => {
+ const errors: Error[] = [];
+ const consoleErrors: string[] = [];
+ const networkFailures: string[] = [];
+
+ test.beforeEach(({ page }) => {
+ // Clear error logs before each test
+ errors.length = 0;
+ consoleErrors.length = 0;
+ networkFailures.length = 0;
+
+ // 1. Catch unhandled exceptions
+ page.on('pageerror', (exception) => {
+ console.error(`[Page Error] ${exception.stack || exception.message}`);
+ errors.push(exception);
+ });
+
+ // 2. Catch console errors
+ page.on('console', (msg) => {
+ if (msg.type() === 'error') {
+ console.error(`[Console Error] ${msg.text()}`);
+ consoleErrors.push(msg.text());
+ }
+ });
+
+ // 3. Catch network failures and incorrect MIME-types
+ page.on('response', (response) => {
+ const status = response.status();
+ const url = response.url();
+
+ if (status >= 400) {
+ const failure = `${url} (HTTP ${status})`;
+ console.error(`[Network Failure] ${failure}`);
+ networkFailures.push(failure);
+ }
+
+ // Verify that Javascript assets are served with correct MIME-types to prevent loading crashes
+ const contentType = response.headers()['content-type'] || '';
+ if (url.endsWith('.js') && !contentType.includes('javascript')) {
+ const failure = `${url} (Invalid MIME: "${contentType}", expected "application/javascript")`;
+ console.error(`[MIME Type Error] ${failure}`);
+ networkFailures.push(failure);
+ }
+ });
+ });
+
+ test('should boot successfully with clean logs and correct visuals', async ({ page }) => {
+ // Navigate to the app root
+ await page.goto('/');
+
+ // Assert that the main drag-and-drop area is visible
+ const dropzone = page.locator('.dropzone');
+ await expect(dropzone).toBeVisible();
+
+ const dropzoneText = await dropzone.locator('p').innerText();
+ expect(dropzoneText).toContain('Drag & drop an aecdump/protobuf file here');
+
+ // Assert there are no unhandled JS exceptions
+ expect(errors, 'Uncaught exceptions were thrown during load').toHaveLength(0);
+
+ // Assert there are no console errors
+ expect(consoleErrors, 'Errors were logged to the browser console').toHaveLength(0);
+
+ // Assert there are no network or MIME-type resolution failures
+ expect(networkFailures, 'Asset requests failed to load').toHaveLength(0);
+ });
+});
diff --git a/src/aecdump-viewer/tsconfig.json b/src/aecdump-viewer/tsconfig.json
new file mode 100644
index 0000000..011a79a
--- /dev/null
+++ b/src/aecdump-viewer/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "es2021",
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "noEmitOnError": true,
+ "lib": ["es2021", "dom"],
+ "strict": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "experimentalDecorators": true,
+ "importHelpers": true,
+ "outDir": "out-tsc",
+ "allowJs": true,
+ "sourceMap": true,
+ "inlineSources": true,
+ "rootDir": "./",
+ "incremental": true
+ },
+ "include": ["**/*.ts", "src/proto/debug.js"]
+}
diff --git a/src/aecdump-viewer/vite.config.ts b/src/aecdump-viewer/vite.config.ts
new file mode 100644
index 0000000..382d9d4
--- /dev/null
+++ b/src/aecdump-viewer/vite.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ server: {
+ // Allow any host to respond to proxy requests (e.g. from cloud workspace / corporate proxy)
+ allowedHosts: true,
+
+ // Listen on all local IP addresses (0.0.0.0) so the proxy can forward requests to Vite
+ host: true,
+ },
+ preview: {
+ // Also configure preview server (used for production build testing)
+ allowedHosts: true,
+ host: true,
+ }
+});
diff --git a/src/aecdump-viewer/yarn.lock b/src/aecdump-viewer/yarn.lock
new file mode 100644
index 0000000..5257b10
--- /dev/null
+++ b/src/aecdump-viewer/yarn.lock
@@ -0,0 +1,957 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/helper-string-parser@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
+ integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
+
+"@babel/helper-validator-identifier@^7.28.5":
+ version "7.28.5"
+ resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
+ integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
+
+"@babel/parser@^7.20.15":
+ version "7.29.3"
+ resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz#116f70a77958307fceac27747573032f8a62f88e"
+ integrity sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==
+ dependencies:
+ "@babel/types" "^7.29.0"
+
+"@babel/types@^7.29.0":
+ version "7.29.0"
+ resolved "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7"
+ integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==
+ dependencies:
+ "@babel/helper-string-parser" "^7.27.1"
+ "@babel/helper-validator-identifier" "^7.28.5"
+
+"@esbuild/aix-ppc64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
+ integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
+
+"@esbuild/android-arm64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
+ integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
+
+"@esbuild/android-arm@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
+ integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
+
+"@esbuild/android-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
+ integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
+
+"@esbuild/darwin-arm64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
+ integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
+
+"@esbuild/darwin-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
+ integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
+
+"@esbuild/freebsd-arm64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
+ integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
+
+"@esbuild/freebsd-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
+ integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
+
+"@esbuild/linux-arm64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
+ integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
+
+"@esbuild/linux-arm@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
+ integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
+
+"@esbuild/linux-ia32@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
+ integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
+
+"@esbuild/linux-loong64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
+ integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
+
+"@esbuild/linux-mips64el@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
+ integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
+
+"@esbuild/linux-ppc64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
+ integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
+
+"@esbuild/linux-riscv64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
+ integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
+
+"@esbuild/linux-s390x@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
+ integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
+
+"@esbuild/linux-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
+ integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
+
+"@esbuild/netbsd-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
+ integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
+
+"@esbuild/openbsd-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
+ integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
+
+"@esbuild/sunos-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
+ integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
+
+"@esbuild/win32-arm64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
+ integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
+
+"@esbuild/win32-ia32@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
+ integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
+
+"@esbuild/win32-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
+ integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
+
+"@jsdoc/salty@^0.2.1":
+ version "0.2.12"
+ resolved "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.12.tgz#6320bb4bd16e98c2da2c666ec97d1a72922cd06a"
+ integrity sha512-TuB0x50EoAvEX/UEWITd8Mkn3WhiTjSvbTMCLj0BhsQEl5iUzjXdA0bETEVpTk+5TGTLR6QktI9H4hLviVeaAQ==
+ dependencies:
+ lodash "^4.18.1"
+
+"@lit-labs/ssr-dom-shim@^1.5.0":
+ version "1.6.0"
+ resolved "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz#693e129b809741fd23e98fcb57e41fd3d082db1a"
+ integrity sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==
+
+"@lit/reactive-element@^2.1.0":
+ version "2.1.2"
+ resolved "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz#4c6af9042603c98e61ba90b294607904d51b61cb"
+ integrity sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==
+ dependencies:
+ "@lit-labs/ssr-dom-shim" "^1.5.0"
+
+"@playwright/test@^1.41.2":
+ version "1.60.0"
+ resolved "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz#e696c31427e8882851235cd556dc2490c3206d97"
+ integrity sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==
+ dependencies:
+ playwright "1.60.0"
+
+"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
+ integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==
+
+"@protobufjs/base64@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
+ integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
+
+"@protobufjs/codegen@^2.0.5":
+ version "2.0.5"
+ resolved "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz#d9315ad7cf3f30aac70bda3c068443dc6f143659"
+ integrity sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==
+
+"@protobufjs/eventemitter@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
+ integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==
+
+"@protobufjs/fetch@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz#4d6fc00c8fb64016a5c81b469d549046350f1065"
+ integrity sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==
+ dependencies:
+ "@protobufjs/aspromise" "^1.1.1"
+
+"@protobufjs/float@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
+ integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==
+
+"@protobufjs/inquire@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz#ae64fbc014ff44c8bfad03dd4c93cd2d6a4c82db"
+ integrity sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==
+
+"@protobufjs/path@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
+ integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==
+
+"@protobufjs/pool@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
+ integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==
+
+"@protobufjs/utf8@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz#eaee5900122c110a3dbcb728c0597014a2621774"
+ integrity sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==
+
+"@rollup/rollup-android-arm-eabi@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz#3a04f01e9f01392bbef5920b94aa3b88794be7ab"
+ integrity sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==
+
+"@rollup/rollup-android-arm64@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz#e371b653ceabc900790ae73f5548a0fd7cd63a70"
+ integrity sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==
+
+"@rollup/rollup-darwin-arm64@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz#2a5aa70432e39816d666d79287a7324cfc3b4e72"
+ integrity sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==
+
+"@rollup/rollup-darwin-x64@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz#c3b5b49629379cd9cdc5d841bf00ed44ebf393dd"
+ integrity sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==
+
+"@rollup/rollup-freebsd-arm64@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz#f929d8e0462fae6602fc960beeabd7287d859283"
+ integrity sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==
+
+"@rollup/rollup-freebsd-x64@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz#c01cb58031226f95d0900b1ec847f4fb32c6e809"
+ integrity sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz#f29d890c4858c8e0d3be01677eef4f6a359eed9d"
+ integrity sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==
+
+"@rollup/rollup-linux-arm-musleabihf@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz#1ebfc8eb9f66136ed2faae5f44995add5ca3c964"
+ integrity sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==
+
+"@rollup/rollup-linux-arm64-gnu@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz#c1fa823c2c4ce46ba7f61de1a4c3fdadd4fb4e7b"
+ integrity sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==
+
+"@rollup/rollup-linux-arm64-musl@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz#a7f18854d0471b78bda8ea38f0891a4e059b571d"
+ integrity sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==
+
+"@rollup/rollup-linux-loong64-gnu@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz#83658a9a4576bcce8cef85b2c78b9b649d2200c4"
+ integrity sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==
+
+"@rollup/rollup-linux-loong64-musl@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz#fd2af677ae3417bb58d57ae37dd0d84686e40244"
+ integrity sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==
+
+"@rollup/rollup-linux-ppc64-gnu@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz#6481647181c4cf8f1ddbd99f62c84cfc56c1a94a"
+ integrity sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==
+
+"@rollup/rollup-linux-ppc64-musl@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz#18610a1a1550e28a5042ca916f898419540f17f4"
+ integrity sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==
+
+"@rollup/rollup-linux-riscv64-gnu@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz#597bb80465a2621dbe0de0a41c66394a8a7e9a6e"
+ integrity sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==
+
+"@rollup/rollup-linux-riscv64-musl@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz#a2a919a9f927ef7f24a60af77e3cb55f1ad59e4d"
+ integrity sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==
+
+"@rollup/rollup-linux-s390x-gnu@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz#3166f6ceae7df9bbfddf9f36be1937231e13e3c6"
+ integrity sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==
+
+"@rollup/rollup-linux-x64-gnu@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz#23c9bf79771d804fb87415eb0767569f273261e5"
+ integrity sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==
+
+"@rollup/rollup-linux-x64-musl@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz#97941c6b94d67fe25cde0f027c10a19f2d1fdd39"
+ integrity sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==
+
+"@rollup/rollup-openbsd-x64@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz#7aeb7d92e2cd1d399f56daf75c39040b777b6c77"
+ integrity sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==
+
+"@rollup/rollup-openharmony-arm64@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz#925de61ae83bf99aa636e8acea87432e8c0ffaab"
+ integrity sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==
+
+"@rollup/rollup-win32-arm64-msvc@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz#888ab83842721491044c46a7407e1f38f3235bb4"
+ integrity sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==
+
+"@rollup/rollup-win32-ia32-msvc@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz#fa30ac24e3f0232139d2a47500560a28695764d4"
+ integrity sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==
+
+"@rollup/rollup-win32-x64-gnu@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz#223e2bc93f86e0707568e1fadb5b537e50c976c7"
+ integrity sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==
+
+"@rollup/rollup-win32-x64-msvc@4.60.4":
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz#da4f1676d87e2bdf744291b504b0ab79550c3e61"
+ integrity sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==
+
+"@types/estree@1.0.8":
+ version "1.0.8"
+ resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
+ integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
+
+"@types/linkify-it@^5":
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
+ integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
+
+"@types/markdown-it@^14.1.1":
+ version "14.1.2"
+ resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
+ integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
+ dependencies:
+ "@types/linkify-it" "^5"
+ "@types/mdurl" "^2"
+
+"@types/mdurl@^2":
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
+ integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
+
+"@types/node@>=13.7.0":
+ version "25.9.1"
+ resolved "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz#3bda556db500ae4319c08e7fc9ab94f19013ba0b"
+ integrity sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==
+ dependencies:
+ undici-types ">=7.24.0 <7.24.7"
+
+"@types/trusted-types@^2.0.2":
+ version "2.0.7"
+ resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
+ integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
+
+acorn-jsx@^5.3.2:
+ version "5.3.2"
+ resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
+ integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
+
+acorn@^8.9.0:
+ version "8.16.0"
+ resolved "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a"
+ integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
+
+ansi-styles@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+argparse@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+ integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+bluebird@^3.7.2:
+ version "3.7.2"
+ resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+ integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
+brace-expansion@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz#4f41a41190216ee36067ec381526fe9539c4f0ae"
+ integrity sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==
+ dependencies:
+ balanced-match "^1.0.0"
+
+catharsis@^0.9.0:
+ version "0.9.0"
+ resolved "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121"
+ integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==
+ dependencies:
+ lodash "^4.17.15"
+
+chalk@^4.0.0:
+ version "4.1.2"
+ resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+ dependencies:
+ ansi-styles "^4.1.0"
+ supports-color "^7.1.0"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+deep-is@~0.1.3:
+ version "0.1.4"
+ resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
+ integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
+
+entities@^4.4.0:
+ version "4.5.0"
+ resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
+ integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+
+esbuild@^0.21.3:
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
+ integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.21.5"
+ "@esbuild/android-arm" "0.21.5"
+ "@esbuild/android-arm64" "0.21.5"
+ "@esbuild/android-x64" "0.21.5"
+ "@esbuild/darwin-arm64" "0.21.5"
+ "@esbuild/darwin-x64" "0.21.5"
+ "@esbuild/freebsd-arm64" "0.21.5"
+ "@esbuild/freebsd-x64" "0.21.5"
+ "@esbuild/linux-arm" "0.21.5"
+ "@esbuild/linux-arm64" "0.21.5"
+ "@esbuild/linux-ia32" "0.21.5"
+ "@esbuild/linux-loong64" "0.21.5"
+ "@esbuild/linux-mips64el" "0.21.5"
+ "@esbuild/linux-ppc64" "0.21.5"
+ "@esbuild/linux-riscv64" "0.21.5"
+ "@esbuild/linux-s390x" "0.21.5"
+ "@esbuild/linux-x64" "0.21.5"
+ "@esbuild/netbsd-x64" "0.21.5"
+ "@esbuild/openbsd-x64" "0.21.5"
+ "@esbuild/sunos-x64" "0.21.5"
+ "@esbuild/win32-arm64" "0.21.5"
+ "@esbuild/win32-ia32" "0.21.5"
+ "@esbuild/win32-x64" "0.21.5"
+
+escape-string-regexp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
+ integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
+
+escodegen@^1.13.0:
+ version "1.14.3"
+ resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
+ integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==
+ dependencies:
+ esprima "^4.0.1"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+eslint-visitor-keys@^3.4.1:
+ version "3.4.3"
+ resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
+ integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
+
+espree@^9.0.0:
+ version "9.6.1"
+ resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
+ integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
+ dependencies:
+ acorn "^8.9.0"
+ acorn-jsx "^5.3.2"
+ eslint-visitor-keys "^3.4.1"
+
+esprima@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+ integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+estraverse@^4.2.0:
+ version "4.3.0"
+ resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+ integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.1.0:
+ version "5.3.0"
+ resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+esutils@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+ integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+fast-levenshtein@~2.0.6:
+ version "2.0.6"
+ resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+ integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+fsevents@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+ integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+fsevents@~2.3.2, fsevents@~2.3.3:
+ version "2.3.3"
+ resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+glob@^8.0.0:
+ version "8.1.0"
+ resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
+ integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^5.0.1"
+ once "^1.3.0"
+
+graceful-fs@^4.1.9:
+ version "4.2.11"
+ resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2:
+ version "2.0.4"
+ resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+js2xmlparser@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz#2a1fdf01e90585ef2ae872a01bc169c6a8d5e60a"
+ integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==
+ dependencies:
+ xmlcreate "^2.0.4"
+
+jsdoc@^4.0.0:
+ version "4.0.5"
+ resolved "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz#fbed70e04a3abcf2143dad6b184947682bbc7315"
+ integrity sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==
+ dependencies:
+ "@babel/parser" "^7.20.15"
+ "@jsdoc/salty" "^0.2.1"
+ "@types/markdown-it" "^14.1.1"
+ bluebird "^3.7.2"
+ catharsis "^0.9.0"
+ escape-string-regexp "^2.0.0"
+ js2xmlparser "^4.0.2"
+ klaw "^3.0.0"
+ markdown-it "^14.1.0"
+ markdown-it-anchor "^8.6.7"
+ marked "^4.0.10"
+ mkdirp "^1.0.4"
+ requizzle "^0.2.3"
+ strip-json-comments "^3.1.0"
+ underscore "~1.13.2"
+
+klaw@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146"
+ integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==
+ dependencies:
+ graceful-fs "^4.1.9"
+
+levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+linkify-it@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
+ integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
+ dependencies:
+ uc.micro "^2.0.0"
+
+lit-element@^4.2.0:
+ version "4.2.2"
+ resolved "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz#f74fcbfbea945eae5614ece22a674fa52ca3365b"
+ integrity sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==
+ dependencies:
+ "@lit-labs/ssr-dom-shim" "^1.5.0"
+ "@lit/reactive-element" "^2.1.0"
+ lit-html "^3.3.0"
+
+lit-html@^3.3.0:
+ version "3.3.3"
+ resolved "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz#a63fd02fb8c1c7b7057ee805ab6c612fdebef0b1"
+ integrity sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==
+ dependencies:
+ "@types/trusted-types" "^2.0.2"
+
+lit@^3.1.1:
+ version "3.3.3"
+ resolved "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz#93579885e51a20a772c68482a34706fe1636e8f0"
+ integrity sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==
+ dependencies:
+ "@lit/reactive-element" "^2.1.0"
+ lit-element "^4.2.0"
+ lit-html "^3.3.0"
+
+lodash@^4.17.15, lodash@^4.17.21, lodash@^4.18.1:
+ version "4.18.1"
+ resolved "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c"
+ integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==
+
+long@^5.3.2:
+ version "5.3.2"
+ resolved "https://registry.npmjs.org/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83"
+ integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==
+
+markdown-it-anchor@^8.6.7:
+ version "8.6.7"
+ resolved "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634"
+ integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==
+
+markdown-it@^14.1.0:
+ version "14.1.1"
+ resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz#856f90b66fc39ae70affd25c1b18b581d7deee1f"
+ integrity sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==
+ dependencies:
+ argparse "^2.0.1"
+ entities "^4.4.0"
+ linkify-it "^5.0.0"
+ mdurl "^2.0.0"
+ punycode.js "^2.3.1"
+ uc.micro "^2.1.0"
+
+marked@^4.0.10:
+ version "4.3.0"
+ resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3"
+ integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==
+
+mdurl@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
+ integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
+
+minimatch@^5.0.1:
+ version "5.1.9"
+ resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b"
+ integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==
+ dependencies:
+ brace-expansion "^2.0.1"
+
+minimist@^1.2.0:
+ version "1.2.8"
+ resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+mkdirp@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+ integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
+nanoid@^3.3.12:
+ version "3.3.12"
+ resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz#ab3d912e217a6d0a514f00a72a16543a28982c05"
+ integrity sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==
+
+once@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+ dependencies:
+ wrappy "1"
+
+optionator@^0.8.1:
+ version "0.8.3"
+ resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+ integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.6"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ word-wrap "~1.2.3"
+
+picocolors@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+ integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
+playwright-core@1.60.0:
+ version "1.60.0"
+ resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz#24e0d9cc4730713db5dffcace29b5e4696b1907a"
+ integrity sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==
+
+playwright@1.60.0:
+ version "1.60.0"
+ resolved "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz#89710863a51f21112633ef8b6b182594d3bfd7b5"
+ integrity sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==
+ dependencies:
+ playwright-core "1.60.0"
+ optionalDependencies:
+ fsevents "2.3.2"
+
+postcss@^8.4.43:
+ version "8.5.15"
+ resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c"
+ integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==
+ dependencies:
+ nanoid "^3.3.12"
+ picocolors "^1.1.1"
+ source-map-js "^1.2.1"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+ integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
+
+protobufjs-cli@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.3.0.tgz#a5849b91421485d2a504206456662ff1e73c8d57"
+ integrity sha512-vIb5o20PPMDH6dw9jPhO1kbC9UJEQor3ENtMOkNi4AjRp5FsrNtqrWgRRNTUcFcnUNhmil54Y1W3fxGADqu0kA==
+ dependencies:
+ chalk "^4.0.0"
+ escodegen "^1.13.0"
+ espree "^9.0.0"
+ estraverse "^5.1.0"
+ glob "^8.0.0"
+ jsdoc "^4.0.0"
+ minimist "^1.2.0"
+ semver "^7.1.2"
+ tmp "^0.2.1"
+ uglify-js "^3.7.7"
+
+protobufjs@^7.2.4:
+ version "7.6.0"
+ resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz#61e42285beec2708a9c84d7abbb5f22e2ddc54d4"
+ integrity sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==
+ dependencies:
+ "@protobufjs/aspromise" "^1.1.2"
+ "@protobufjs/base64" "^1.1.2"
+ "@protobufjs/codegen" "^2.0.5"
+ "@protobufjs/eventemitter" "^1.1.0"
+ "@protobufjs/fetch" "^1.1.1"
+ "@protobufjs/float" "^1.0.2"
+ "@protobufjs/inquire" "^1.1.2"
+ "@protobufjs/path" "^1.1.2"
+ "@protobufjs/pool" "^1.1.0"
+ "@protobufjs/utf8" "^1.1.1"
+ "@types/node" ">=13.7.0"
+ long "^5.3.2"
+
+punycode.js@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
+ integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
+
+requizzle@^0.2.3:
+ version "0.2.4"
+ resolved "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c"
+ integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==
+ dependencies:
+ lodash "^4.17.21"
+
+rollup@^4.20.0:
+ version "4.60.4"
+ resolved "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz#ca3814f5900da3ac3981d2e0c61944b7e6e0cb09"
+ integrity sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==
+ dependencies:
+ "@types/estree" "1.0.8"
+ optionalDependencies:
+ "@rollup/rollup-android-arm-eabi" "4.60.4"
+ "@rollup/rollup-android-arm64" "4.60.4"
+ "@rollup/rollup-darwin-arm64" "4.60.4"
+ "@rollup/rollup-darwin-x64" "4.60.4"
+ "@rollup/rollup-freebsd-arm64" "4.60.4"
+ "@rollup/rollup-freebsd-x64" "4.60.4"
+ "@rollup/rollup-linux-arm-gnueabihf" "4.60.4"
+ "@rollup/rollup-linux-arm-musleabihf" "4.60.4"
+ "@rollup/rollup-linux-arm64-gnu" "4.60.4"
+ "@rollup/rollup-linux-arm64-musl" "4.60.4"
+ "@rollup/rollup-linux-loong64-gnu" "4.60.4"
+ "@rollup/rollup-linux-loong64-musl" "4.60.4"
+ "@rollup/rollup-linux-ppc64-gnu" "4.60.4"
+ "@rollup/rollup-linux-ppc64-musl" "4.60.4"
+ "@rollup/rollup-linux-riscv64-gnu" "4.60.4"
+ "@rollup/rollup-linux-riscv64-musl" "4.60.4"
+ "@rollup/rollup-linux-s390x-gnu" "4.60.4"
+ "@rollup/rollup-linux-x64-gnu" "4.60.4"
+ "@rollup/rollup-linux-x64-musl" "4.60.4"
+ "@rollup/rollup-openbsd-x64" "4.60.4"
+ "@rollup/rollup-openharmony-arm64" "4.60.4"
+ "@rollup/rollup-win32-arm64-msvc" "4.60.4"
+ "@rollup/rollup-win32-ia32-msvc" "4.60.4"
+ "@rollup/rollup-win32-x64-gnu" "4.60.4"
+ "@rollup/rollup-win32-x64-msvc" "4.60.4"
+ fsevents "~2.3.2"
+
+semver@^7.1.2:
+ version "7.8.0"
+ resolved "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz#ed0661039fcbcda2ce71f01fa6adbefaa77040df"
+ integrity sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==
+
+source-map-js@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
+ integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
+source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+strip-json-comments@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+ integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
+tmp@^0.2.1:
+ version "0.2.5"
+ resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8"
+ integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==
+
+tslib@^2.6.2:
+ version "2.8.1"
+ resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==
+ dependencies:
+ prelude-ls "~1.1.2"
+
+typescript@^5.3.3:
+ version "5.9.3"
+ resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
+ integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
+
+uc.micro@^2.0.0, uc.micro@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
+ integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
+
+uglify-js@^3.7.7:
+ version "3.19.3"
+ resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f"
+ integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==
+
+underscore@~1.13.2:
+ version "1.13.8"
+ resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz#a93a21186c049dbf0e847496dba72b7bd8c1e92b"
+ integrity sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==
+
+"undici-types@>=7.24.0 <7.24.7":
+ version "7.24.6"
+ resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz#61275b485d7fd4e9d269c7cf04ec2873c9cc0f91"
+ integrity sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==
+
+vite@^5.0.12:
+ version "5.4.21"
+ resolved "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz#84a4f7c5d860b071676d39ba513c0d598fdc7027"
+ integrity sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==
+ dependencies:
+ esbuild "^0.21.3"
+ postcss "^8.4.43"
+ rollup "^4.20.0"
+ optionalDependencies:
+ fsevents "~2.3.3"
+
+wavesurfer.js@^7.7.15:
+ version "7.12.7"
+ resolved "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.12.7.tgz#31f66428a11013d49817a060481d5f82f58ea1da"
+ integrity sha512-TIe7hB6OCZysNOZ2cn2NR8Qpko22POWel6rauNcqOammFoH65NYQUM35unNLLMIlUMVYvjJ6w/TTl/G/m+w0nA==
+
+word-wrap@~1.2.3:
+ version "1.2.5"
+ resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
+ integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+xmlcreate@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be"
+ integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==
diff --git a/src/index.html b/src/index.html
index 958b026..05329f0 100644
--- a/src/index.html
+++ b/src/index.html
@@ -14,6 +14,7 @@ Audio tests and demos
Microphone Testing
Volume Tuning
Device Selection Playground
+ AECDump Web Viewer
WebAudio Delay (external)
PWA Audio Recorder