> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ai-coustics.com/llms.txt
> Use this file to discover all available pages before exploring further.

# WebAssembly

> Process browser audio with the released WASM package and keep SDK keys on your backend.

The **WebAssembly binding** runs models in the browser. This guide uses [npm package 0.25.0](https://www.npmjs.com/package/@ai-coustics/aic-sdk-wasm/v/0.25.0). Use the [WebAssembly API reference](/reference/sdk/api/wasm/index) for initialization, classes and resource cleanup. The package includes the released `aic_sdk_wasm.d.ts` type declarations. Use the [compatibility matrix](/reference/sdk/compatibility-matrix) when selecting models and upgrading.

| API | Contract in 0.25.0 |
| - | - |
| `init()` | Asynchronously loads the WASM module before SDK calls |
| `Model.fromBytes(bytes)` | Loads a compatible model from a `Uint8Array` |
| `Processor.process(frame)` | Synchronous, in-place mono enhancement |
| `getContext()` | Returns controls for delay, reset, parameters and JWT refresh |
| `terminateSession()` and `free()` | End session activity and release owned WASM resources |

The package also exports dedicated `Vad` and `Analyzer` APIs. Feed VAD the original input; do not infer that another binding's async or telemetry configuration API exists in WASM. An enhancement processor can also report speech through [`getEnergyVadContext()`](/reference/sdk/api/wasm/enhancement#wasm-processor-getenergyvadcontext), without a second model. See [authentication](/models/get-started/authenticate-apps) and [streams and state](/reference/concepts/streams-and-state).

## Installation

Use `@ai-coustics/aic-sdk-wasm` **0.25.0**, Node.js 22 or later for the local token server and a browser with WebAssembly SIMD support. This example processes a local recording into a mono WAV; it does not use an AudioWorklet or microphone pipeline.

Download the [noisy speech fixture](/guides/test-audio), or use a short recording you have permission to process, exported as mono PCM16 WAV at 16 kHz. The example accepts up to 60 seconds. Create an SDK key in the [developer platform](https://developers.ai-coustics.com) and put it in the server's `AIC_SDK_LICENSE` environment variable. Do not put the key into HTML, browser storage or client configuration.

```bash theme={null}
mkdir aic-wasm-example
cd aic-wasm-example
npm init -y
npm install --save-exact @ai-coustics/aic-sdk-wasm@0.25.0
curl --fail --location \
  'https://artifacts.ai-coustics.io/models/quail-vf-2-2-s-16khz/v7/quail_vf_2_2_s_16khz_gf70x7zf_v14.aicmodel' \
  --output model.aicmodel
```

Set the server credential in this terminal without putting it in shell history:

```bash theme={null}
read -r -s AIC_SDK_LICENSE
export AIC_SDK_LICENSE
```

Paste your SDK key at the hidden prompt and press Enter. The key remains on the server; the browser receives a short-lived JSON Web Token (JWT).

## Quickstart

### Create the local token server

Save the following as `server.mjs`. This server binds to loopback and serves only the listed files. The token endpoint checks the request origin and returns no cached credentials. It decodes the SDK key on the server using the [documented token flow](/models/get-started/authenticate-apps).

<Warning>
  This is a local development server. Before production, replace it with an HTTPS backend that authenticates users, authorizes SDK access and enforces rate and usage limits. Never expose this demo server on a public interface.
</Warning>

```javascript server.mjs theme={null}
import http from 'node:http';
import { readFile } from 'node:fs/promises';

const origin = 'http://127.0.0.1:4173';
const sdkKey = process.env.AIC_SDK_LICENSE;
if (!sdkKey) throw new Error('Set AIC_SDK_LICENSE in the server environment.');
const files = new Map([
  ['/', ['index.html', 'text/html']],
  ['/sdk.js', ['node_modules/@ai-coustics/aic-sdk-wasm/aic_sdk_wasm.js', 'text/javascript']],
  ['/aic_sdk_wasm_bg.wasm', ['node_modules/@ai-coustics/aic-sdk-wasm/aic_sdk_wasm_bg.wasm', 'application/wasm']],
  ['/model.aicmodel', ['model.aicmodel', 'application/octet-stream']],
]);
const server = http.createServer(async (req, res) => {
  res.setHeader('Cache-Control', 'no-store');
  res.setHeader('X-Content-Type-Options', 'nosniff');
  if (req.headers.host !== '127.0.0.1:4173') {
    res.writeHead(403).end('Use the loopback URL printed by the server.');
    return;
  }
  if (req.url === '/session-token' && req.method === 'POST') {
    // This is a local-only demo. Production needs authenticated users and quotas.
    if (req.headers.origin !== origin || req.headers['x-aic-demo'] !== '1') {
      res.writeHead(403).end('Same-origin demo request required.');
      return;
    }
    try {
      const payload = JSON.parse(Buffer.from(sdkKey.split('.')[0], 'base64').toString());
      if (typeof payload.api_key !== 'string') throw new Error('Invalid SDK key');
      const basic = Buffer.from(`${payload.api_key}:`).toString('base64');
      const response = await fetch('https://api.ai-coustics.io/v1/sdk/tokens', {
        method: 'POST', headers: { Authorization: `Basic ${basic}` },
        signal: AbortSignal.timeout(10000),
      });
      if (!response.ok) throw new Error('Token request rejected');
      const { token } = await response.json();
      if (typeof token !== 'string') throw new Error('Missing token');
      const { exp } = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
      if (!Number.isFinite(exp) || exp * 1000 <= Date.now() + 120000) {
        throw new Error('Token lifetime is too short for this demo');
      }
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ token }));
    } catch {
      // Do not log credentials or return upstream response bodies.
      res.writeHead(502).end('Token request failed. Check the server key and connectivity.');
    }
    return;
  }
  const file = files.get(req.url);
  if (req.method !== 'GET' || !file) {
    res.writeHead(404).end('Not found');
    return;
  }
  try {
    const bytes = await readFile(new URL(file[0], import.meta.url));
    res.writeHead(200, { 'Content-Type': file[1] }).end(bytes);
  } catch {
    res.writeHead(404).end('File missing. Check installation and model download.');
  }
});
server.listen(4173, '127.0.0.1', () => console.log(`Open ${origin}`));
```

### Create the browser page

Save this as `index.html` beside `server.mjs`. Audio stays in browser memory. The page loads the WASM module and model, requests a token, processes complete frames, pads the tail and removes the reported delay before creating the output file. Session authorization and usage reporting still require network access.

```html index.html theme={null}
<!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.getContext();
    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>
```

### Run and check the result

```bash theme={null}
node server.mjs
```

Open `http://127.0.0.1:4173`, select your recording and choose **Enhance**. On success, the page displays the number of processed samples and SDK version and exposes **Download enhanced.wav**. The output has the same sample count as the input, one channel and a 16 kHz sample rate. Compare both files to evaluate quality for your task.

Each recording uses a fresh token with at least two minutes of remaining validity. Processing stops after one minute of wall time. These are demo bounds, not SDK limits. Stop the server with Ctrl+C, then run `unset AIC_SDK_LICENSE`.

## Recover from an error

| Problem | Cause and retry |
| - | - |
| Server asks for `AIC_SDK_LICENSE` | Set the key in the server's terminal and restart it. |
| Module or model file is missing | Run the installation/download commands in the same directory as `server.mjs`. |
| Browser cannot compile WASM | Use a browser with WebAssembly SIMD support and verify that the `.wasm` response is served as `application/wasm`. |
| Token unavailable | Check the server key, token-service connectivity and whether the credential can mint a JWT. Do not copy a long-lived key into the browser as a workaround. |
| `ProcessingNotAllowed` | Check activation connectivity, credential expiry and authorization. Correct the cause and retry with a fresh token. Do not treat a stopped or bypassed run as enhancement. |
| Invalid WAV or empty input | Export nonempty mono PCM16 audio at 16 kHz and select the corrected file. |
| Timeout | Try a shorter recording and measure your target device's processing budget before a live integration. |

## Adapt this example

Use one processor per independent audio stream. `Processor.process()` mutates its `Float32Array` in place and runs synchronously. Move sustained processing off the main UI thread and design buffering around the [audio format](/reference/concepts/audio-format) and [latency](/reference/concepts/latency) contracts.

For a long-running stream, refresh a JWT through your authenticated backend before expiry and update the existing context:

```javascript theme={null}
// context came from processor.getContext().
// freshToken came from your authenticated backend.
context.updateBearerToken(freshToken);
```

Both the original and replacement credentials must be JWTs. A successful local update does not prove backend acceptance; handle subsequent processing errors. Keep only the current short-lived token in memory. Never log it or save it to local storage. At shutdown, free the context and model, terminate the session and free the processor as shown above.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.