Files
XZBT/tools/audio-acceptance.js

136 lines
7.3 KiB
JavaScript

// Bundled by build-audio-acceptance.mjs with the production runtime and fixtures.
const acceptanceElement = id => document.getElementById(id);
let acceptanceAudio = null, acceptanceTimer = null, acceptanceRunning = false;
let acceptanceMeasurement = null, acceptanceDiagnostics = null;
const acceptanceListeningLog = [];
const acceptanceStatus = message => { acceptanceElement('state').textContent = message; };
function acceptanceLock(running) {
acceptanceRunning = running;
acceptanceElement('capture').disabled = running;
for (const button of acceptanceElement('sounds').querySelectorAll('button')) button.disabled = running;
for (const id of ['release', 'pause', 'resume']) acceptanceElement(id).disabled = running;
}
async function acceptanceStop() {
clearInterval(acceptanceTimer); acceptanceTimer = null;
const previous = acceptanceAudio; acceptanceAudio = null;
if (previous?.context) previous.context.onstatechange = null;
await previous?.dispose();
}
function acceptanceMakeAudio(fixture) {
const result = validateExhibit(fixture);
if (!result.valid) throw new Error(JSON.stringify(result.errors));
acceptanceDiagnostics = new Diagnostics();
const audio = new AudioSubsystem({ document: fixture, rng: new SeededRNG(42), diagnostics: acceptanceDiagnostics });
audio.setMasterVolume(1);
acceptanceAudio = audio;
return audio;
}
function acceptanceEnvironment() {
return {
notes: acceptanceElement('environment').value,
userAgent: navigator.userAgent,
viewport: { width: innerWidth, height: innerHeight },
devicePixelRatio, hardwareConcurrency: navigator.hardwareConcurrency,
deviceMemoryGiB: navigator.deviceMemory ?? null,
protocol: location.protocol
};
}
acceptanceElement('build').textContent = JSON.stringify(ACCEPTANCE_BUILD, null, 2);
acceptanceElement('capture').onclick = async () => {
acceptanceLock(true);
acceptanceMeasurement = null;
acceptanceElement('measurement').textContent = 'Initializing protected audio…';
await acceptanceStop();
let audio;
try {
audio = acceptanceMakeAudio(STRESS);
if (!await audio.unlock()) throw new Error('Protected audio could not start. See diagnostics in the report.');
const context = audio.context, bursts = [], start = context.currentTime;
context.onstatechange = () => {
if (context.state !== 'running') void acceptanceStop();
};
const environment = acceptanceEnvironment();
for (let i = 0; i < 16; i++) if (!audio.play('bed')) throw new Error('Continuous stress voice refused.');
const capture = audio.captureOutput();
// Attach a rejection handler immediately, including synchronous burst failures.
capture.catch(() => {});
const burst = () => {
for (let i = 0; i < 64; i++) if (!audio.play('hit')) throw new Error('Transient stress voice refused.');
bursts.push({ seconds: context.currentTime - start, oneshots: audio.oneshotVoices.size, continuous: audio.continuousVoices.size,
expandedGraphNodes: [...audio.oneshotVoices, ...audio.continuousVoices].reduce((count, voice) => count + (voice.plan?.nodes.length ?? 0), 0) });
const elapsed = context.currentTime - start;
acceptanceStatus(elapsed < 30 ? `Warming up · ${elapsed.toFixed(0)} / 30 seconds` : `Capturing · ${(elapsed - 30).toFixed(0)} / 120 seconds`);
};
burst();
acceptanceTimer = setInterval(() => {
try { burst(); } catch (error) { acceptanceStatus(error.message); void acceptanceStop(); }
}, 2000);
const result = await capture;
const peakDb = result.peak > 0 ? 20 * Math.log10(result.peak) : null;
acceptanceMeasurement = {
capturedAt: new Date().toISOString(), environment, ...result, peakDb,
ceilingDb: AUDIO_PROTECTION.ceilingDb, toleranceDb: AUDIO_PROTECTION.toleranceDb,
lookaheadFrames: Math.ceil(context.sampleRate * AUDIO_PROTECTION.lookaheadMs / 1000),
baseLatency: context.baseLatency ?? null, outputLatency: context.outputLatency ?? null,
bursts, maxBurstGapSeconds: Math.max(0, ...bursts.slice(1).map((item, i) => item.seconds - bursts[i].seconds)),
candidateDigitalPeakPass: result.peak > 0.01 && peakDb <= AUDIO_PROTECTION.ceilingDb + AUDIO_PROTECTION.toleranceDb && result.affectedBlocks === 0,
diagnostics: acceptanceDiagnostics.list(),
acceptance: 'Pending user review of workload timing, environment, listening, and production reference/challenge runs.'
};
acceptanceElement('measurement').textContent = JSON.stringify(acceptanceMeasurement, null, 2);
acceptanceStatus('Digital capture finished. Record listening observations and download the report.');
} catch (error) {
acceptanceMeasurement = { interrupted: true, reason: error.message, diagnostics: acceptanceDiagnostics?.list() ?? [] };
acceptanceElement('measurement').textContent = JSON.stringify(acceptanceMeasurement, null, 2);
acceptanceStatus(`Capture incomplete: ${error.message}`);
} finally {
await acceptanceStop();
acceptanceLock(false);
}
};
acceptanceElement('cancel').onclick = async () => {
await acceptanceStop();
acceptanceListeningLog.push({ action: 'stop', at: new Date().toISOString() });
acceptanceStatus('Audio stopped.');
};
for (const [id, sound] of Object.entries(CHALLENGE.sounds)) {
const button = document.createElement('button'); button.textContent = sound.name;
button.onclick = async () => {
try {
if (!acceptanceAudio) acceptanceMakeAudio(CHALLENGE);
const audio = acceptanceAudio;
if (!await audio.unlock()) throw new Error('Protected audio could not start.');
if (!audio.play(id)) throw new Error('Voice could not start; release or stop existing voices.');
acceptanceListeningLog.push({ action: 'play', sound: id, at: new Date().toISOString() });
acceptanceStatus(`Playing ${sound.name}.`);
} catch (error) { acceptanceStatus(error.message); }
};
acceptanceElement('sounds').append(button);
}
acceptanceElement('release').onclick = () => {
acceptanceAudio?.stopAll();
acceptanceListeningLog.push({ action: 'release', at: new Date().toISOString() });
acceptanceStatus('All voices releasing.');
};
for (const [id, method] of [['pause', 'suspend'], ['resume', 'resume']]) {
acceptanceElement(id).onclick = async () => {
try {
await acceptanceAudio?.context?.[method]();
acceptanceListeningLog.push({ action: id, at: new Date().toISOString() });
acceptanceStatus(id === 'pause' ? 'Audio paused.' : 'Audio resumed.');
} catch (error) { acceptanceStatus(error.message); }
};
}
document.addEventListener('visibilitychange', () => {
if (document.hidden && acceptanceRunning) void acceptanceStop();
});
acceptanceElement('save').onclick = () => {
const report = { build: ACCEPTANCE_BUILD, exportedAt: new Date().toISOString(), environment: acceptanceEnvironment(),
measurement: acceptanceMeasurement, listeningLog: acceptanceListeningLog,
listeningObservations: acceptanceElement('observations').value, diagnostics: acceptanceDiagnostics?.list() ?? [],
phase3Acceptance: 'Pending user review; this report does not automatically close GC6 or PRD 129.' };
const url = URL.createObjectURL(new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' }));
const anchor = document.createElement('a'); anchor.href = url; anchor.download = 'xzbt-phase3c4-evidence.json'; anchor.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
};