<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ai-coustics WASM file example</title>
<h1>Enhance a short recording</h1>
<p>Select a mono, 16 kHz, PCM16 WAV with speech (up to 60 seconds).</p>
<label>Recording <input id="input" type="file" accept=".wav"></label>
<button id="run">Enhance</button>
<p id="status" role="status">Ready.</p>
<a id="download" hidden download="enhanced.wav">Download enhanced.wav</a>
<script type="module">
import init, { Model, Processor, getVersion } from '/sdk.js';
const input = document.querySelector('#input');
const run = document.querySelector('#run');
const status = document.querySelector('#status');
const download = document.querySelector('#download');
let outputUrl;
function readWav(bytes) {
const view = new DataView(bytes);
const text = (offset, length) => String.fromCharCode(...new Uint8Array(bytes, offset, length));
if (bytes.byteLength < 44 || text(0, 4) !== 'RIFF' || text(8, 4) !== 'WAVE') {
throw new Error('Select a RIFF WAV file.');
}
let format, data;
for (let pos = 12; pos + 8 <= bytes.byteLength;) {
const size = view.getUint32(pos + 4, true), start = pos + 8;
if (start + size > bytes.byteLength) throw new Error('Truncated WAV chunk.');
if (text(pos, 4) === 'fmt ' && size >= 16) {
format = [view.getUint16(start, true), view.getUint16(start + 2, true),
view.getUint32(start + 4, true), view.getUint16(start + 14, true)];
}
if (text(pos, 4) === 'data') data = [start, size];
pos = start + size + (size % 2);
}
if (String(format) !== '1,1,16000,16' || !data || data[1] % 2) {
throw new Error('Export mono PCM16 WAV at 16 kHz, then retry.');
}
const samples = new Float32Array(data[1] / 2);
if (!samples.length || samples.length > 16000 * 60) {
throw new Error('Use a nonempty recording of up to 60 seconds.');
}
for (let i = 0; i < samples.length; i++) samples[i] = view.getInt16(data[0] + i * 2, true) / 32768;
return samples;
}
function wavBlob(samples) {
const bytes = new ArrayBuffer(44 + samples.length * 2), view = new DataView(bytes);
const text = (offset, value) => [...value].forEach((c, i) => view.setUint8(offset + i, c.charCodeAt(0)));
text(0, 'RIFF'); view.setUint32(4, bytes.byteLength - 8, true); text(8, 'WAVE');
text(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true);
view.setUint16(22, 1, true); view.setUint32(24, 16000, true); view.setUint32(28, 32000, true);
view.setUint16(32, 2, true); view.setUint16(34, 16, true); text(36, 'data');
view.setUint32(40, samples.length * 2, true);
samples.forEach((x, i) => view.setInt16(44 + i * 2, Math.round(Math.max(-1, Math.min(32767 / 32768, x)) * 32768), true));
return new Blob([bytes], { type: 'audio/wav' });
}
run.onclick = async () => {
let model, processor, context;
run.disabled = true; download.hidden = true;
if (outputUrl) { URL.revokeObjectURL(outputUrl); outputUrl = undefined; }
try {
if (!input.files.length) throw new Error('Select a recording first.');
if (input.files[0].size > 3_000_000) throw new Error('Select a WAV file smaller than 3 MB.');
const samples = readWav(await input.files[0].arrayBuffer());
status.textContent = 'Loading the WASM module and model...';
await init();
const response = await fetch('/model.aicmodel', { signal: AbortSignal.timeout(30000) });
if (!response.ok) throw new Error('Model unavailable. Check the model download.');
model = Model.fromBytes(new Uint8Array(await response.arrayBuffer()));
const tokenResponse = await fetch('/session-token', {
method: 'POST', headers: { 'X-Aic-Demo': '1' }, signal: AbortSignal.timeout(15000),
});
if (!tokenResponse.ok) throw new Error('Token unavailable. Check the server key and connectivity.');
let { token } = await tokenResponse.json();
processor = new Processor(model, token);
token = undefined; // No URL, storage, console or source-code credential.
const size = model.getOptimalBlockSize(16000);
processor.initialize(16000, size, false);
context = processor.getProcessorContext();
const delay = context.getAudioDelay();
const count = Math.ceil((samples.length + delay) / size);
const processed = new Float32Array(count * size);
const deadline = performance.now() + 60000;
status.textContent = 'Processing...';
for (let block = 0; block < count; block++) {
if (performance.now() > deadline) throw new Error('Processing timed out. Use a shorter recording.');
const frame = new Float32Array(size), offset = block * size;
frame.set(samples.subarray(offset, offset + size));
processor.process(frame);
processed.set(frame, offset);
// Allow browser I/O and UI tasks to run between synchronous calls.
if (block % 10 === 0) await new Promise(resolve => setTimeout(resolve, 0));
}
const output = processed.slice(delay, delay + samples.length);
if (!output.every(Number.isFinite)) throw new Error('Non-finite output; stop and check the model.');
outputUrl = URL.createObjectURL(wavBlob(output));
download.href = outputUrl; download.hidden = false;
status.textContent = `Processed ${output.length} samples at 16000 Hz with SDK ${getVersion()}.`;
} catch (error) {
status.textContent = `Stopped: ${error.message.replace(/[.!?]+$/, '')}. Correct the problem and select Enhance to retry.`;
} finally {
context?.free();
if (processor) { processor.terminateSession(); processor.free(); }
model?.free(); run.disabled = false;
}
};
</script>
</html>