136 lines
7.3 KiB
JavaScript
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);
|
|
};
|