> ## 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.

# Node.js

> Process a WAV with Node.js, then integrate synchronous or async native audio processing.

**ai-coustics** Node.js bindings provide native processing with TypeScript declarations. The tutorial below uses `@ai-coustics/aic-sdk` 0.25.0. Use the [Node.js API reference](/reference/sdk/api/node/index) for classes, functions, properties and errors. The released [TypeScript declarations](https://github.com/ai-coustics/aic-sdk-node/blob/0.25.0/index.d.ts) and [examples](https://github.com/ai-coustics/aic-sdk-node/tree/0.25.0/examples) provide source context.

## Installation

Use Node.js 22 for this example. The package declares Node.js 18 or later; see the [compatibility matrix](/reference/sdk/compatibility-matrix) for native platform support. Create an isolated project and install the pinned SDK and WAV reader:

```bash theme={null}
mkdir aic-node-quickstart
cd aic-node-quickstart
npm init -y
npm install --save-exact @ai-coustics/aic-sdk@0.25.0 wavefile@11.0.0
```

## Quickstart

### Prepare the key and input

Generate an SDK key on the [developer platform](https://developers.ai-coustics.com). Set it in the terminal where you will run the example, replacing `YOUR_SDK_KEY` with your key. Keep it out of source control and shared terminal logs.

<CodeGroup>
  ```bash macOS / Linux theme={null}
  export AIC_SDK_LICENSE="YOUR_SDK_KEY"
  ```

  ```powershell Windows PowerShell theme={null}
  $env:AIC_SDK_LICENSE = "YOUR_SDK_KEY"
  ```
</CodeGroup>

Download the [noisy speech fixture](/assets/audio/noisy-speech.wav) and save it as `input.wav` in `aic-node-quickstart`. It contains 56,080 mono PCM16 samples at 16 kHz (3.505 seconds). The [fixture guide](/guides/test-audio) includes attribution, checksums and the aligned clean reference.

For your own recording, export a short sentence with background noise as **mono, 16 kHz, signed 16-bit PCM WAV**. Renaming a file does not convert it.

The example downloads `quail-vf-2.2-l-16khz` into `./models`. Quail Voice Focus isolates the primary speaker for speech-to-text (STT) input. The first run needs network access for the model download and SDK key authorization. See [authentication](/models/get-started/authenticate-apps) for deployment options.

### Save and run

Save this complete program as `quickstart.cjs`. The `.cjs` extension makes its CommonJS module format explicit, including in projects that use `"type": "module"`.

```javascript quickstart.cjs theme={null}
const fs = require("node:fs");
const { WaveFile } = require("wavefile");
const { Model, ProcessorAsync, getVersion } = require("@ai-coustics/aic-sdk");

async function main() {
  const licenseKey = process.env.AIC_SDK_LICENSE;
  if (!licenseKey || licenseKey === "YOUR_SDK_KEY") {
    throw new Error("Set AIC_SDK_LICENSE to your SDK key, then retry.");
  }
  const input = new WaveFile(fs.readFileSync("input.wav"));
  if (input.fmt.numChannels !== 1 || input.fmt.sampleRate !== 16000 || input.bitDepth !== "16") {
    throw new Error("Export input.wav as mono, 16 kHz, 16-bit PCM WAV, then retry.");
  }
  if (input.data.samples.length !== input.data.chunkSize) {
    throw new Error("input.wav is truncated. Export the complete recording, then retry.");
  }
  input.toBitDepth("32f");
  const audio = input.getSamples(false, Float32Array);
  if (audio.length === 0) {
    throw new Error("input.wav is empty. Record speech, then retry.");
  }

  const modelPath = await Model.download("quail-vf-2.2-l-16khz", "./models");
  const model = Model.fromFile(modelPath);
  let processor;
  try {
    const blockSize = model.getOptimalBlockSize(16000);
    processor = new ProcessorAsync(model, licenseKey);
    await processor.initialize(16000, blockSize, false);
    const context = await processor.getContext();
    const delay = context.getAudioDelay();
    // Pad the last block and flush the delayed tail with silence.
    const paddedSize = Math.ceil((audio.length + delay) / blockSize) * blockSize;
    const padded = new Float32Array(paddedSize);
    padded.set(audio);
    const output = new Float32Array(paddedSize);
    for (let start = 0; start < paddedSize; start += blockSize) {
      // Await each block to preserve stream order. Async processing returns a new array.
      const enhanced = await processor.process(padded.subarray(start, start + blockSize));
      output.set(enhanced, start);
    }
    const enhanced = output.subarray(delay, delay + audio.length);
    if (!enhanced.every(Number.isFinite)) {
      throw new Error("Processing returned non-finite samples; no output was written.");
    }
    const pcm = Int16Array.from(enhanced, (sample) =>
      Math.trunc(Math.max(-1, Math.min(32767 / 32768, sample)) * 32768)
    );
    const result = new WaveFile();
    result.fromScratch(1, 16000, "16", pcm);
    fs.writeFileSync("enhanced.wav", result.toBuffer());
    console.log(`SDK ${getVersion()}, model ${model.getId()}`);
    console.log(`Processed ${audio.length} samples at 16000 Hz; wrote enhanced.wav`);
  } finally {
    // All processing promises above have settled before native resources are released.
    if (processor) processor.dispose();
    model.dispose();
  }
}

main().catch((error) => {
  console.error(`Error: ${error.message}`);
  process.exitCode = 1;
});
```

Run it from `aic-node-quickstart`:

```bash theme={null}
node quickstart.cjs
```

### Check the result

The program prints the native SDK version, resolved model ID and processed sample count. It writes `enhanced.wav` beside `input.wav` at 16 kHz with the same sample count, compensating for [processing delay](/reference/concepts/latency).

With the supplied fixture, the processed sample count is `56080`. Verify the file headers:

```bash theme={null}
node -e "const fs=require('node:fs'); const {WaveFile}=require('wavefile'); const a=new WaveFile(fs.readFileSync('input.wav')); const b=new WaveFile(fs.readFileSync('enhanced.wav')); for (const k of ['numChannels','sampleRate','bitsPerSample']) if(a.fmt[k]!==b.fmt[k]) throw Error(k+' differs'); if(a.getSamples().length!==b.getSamples().length) throw Error('sample count differs'); console.log('PASS: matching channels, bit depth, sample rate and frame count');"
```

Listen to both recordings and compare them with the same STT settings to evaluate quality.

### Recover from an error

| Problem | Cause and retry |
| - | - |
| **Native module cannot load** | Check `node --version` and your OS/architecture against the [compatibility matrix](/reference/sdk/compatibility-matrix). Install in the target environment with optional dependencies enabled, then rerun `node quickstart.cjs`. |
| **Missing or invalid SDK key** | Set `AIC_SDK_LICENSE` in this terminal to the complete SDK key from the developer platform, then retry. |
| **Processing is not allowed** | Check key validity, account entitlement and required connectivity in [authentication](/models/get-started/authenticate-apps). Correct the cause and restart the program with a new processor. |
| **Missing or invalid `input.wav`** | Save a nonempty mono, 16 kHz, signed 16-bit PCM WAV in the current directory, then rerun the program. |
| **Model download or load fails** | Check connectivity, write access to `./models` and the exact model ID. For manual model files, check the [model format version](/reference/sdk/compatibility-matrix). Retry the download after fixing the cause. |
| **Audio configuration mismatch** | Keep every call at `blockSize` samples. The example pads the final block; do not pass a short final slice with fixed block sizes. |

### Adapt this example

Use one processor per independent stream and await each operation before submitting the next block. Use separate processors for simultaneous streams; `Promise.all` over blocks from one stream can reorder processing. Downmix stereo input or give each channel its own processor. See [audio format](/reference/concepts/audio-format), [streams and state](/reference/concepts/streams-and-state) and the [Node.js binding guide](/reference/sdk/language-bindings/nodejs#integration-behavior).

This example reads the full file into memory and uses synchronous file I/O. For live audio or long files, process bounded blocks and keep file I/O and model downloads outside the audio path. For human listening, evaluate [Rook Multi Speaker](/models/perceptual-speech-enhancement/rook-multi-speaker).

## Integration behavior

| Concern | Node.js 0.25.0 behavior |
| - | - |
| **Synchronous processing** | `Processor.process(audio)` accepts a mono `Float32Array`, modifies it in place and returns `void`. Use normalized samples, nominally -1.0 to 1.0. It runs on the calling thread. |
| **Async processing** | `await ProcessorAsync.process(audio)` returns a new `Float32Array`; it copies the input before queuing work and leaves that input unchanged. Await `initialize` and `getContext` as well. |
| **Ordering** | Await each operation before the next block on the same processor. Concurrent calls are not guaranteed to execute in submission order. Use one instance per stream. |
| **Initialization** | Construct the processor, then call `initialize(sampleRate, blockSize, false)`. Obtain the optimal block size for the actual input rate with `model.getOptimalBlockSize(sampleRate)`. |
| **Block size** | Every call contains exactly `blockSize` samples by default. Setting the third initialization argument to `true` allows shorter blocks with additional buffering delay. Larger blocks are rejected. |
| **State and parameters** | The context exposes `reset`, `setParameter`, `getParameter` and `getAudioDelay`. Reset on a discontinuity or before unrelated audio; reset preserves audio settings. |
| **Worker configuration** | Async work uses Node's shared libuv thread pool. Set `UV_THREADPOOL_SIZE` before Node starts if your measurements justify it. `AIC_NUM_THREADS` does not configure these bindings. |
| **Cleanup** | Use `dispose()` in `finally` to release native resources. Await outstanding operations first. Async processor disposal is synchronous and may block while a worker holds the instance lock. |
| **Model lifetime** | Multiple independent processors can share a model. `Model.fromFile` memory-maps its file; do not modify or delete that file while any dependent object is alive. |

The package includes TypeScript declarations and is distributed as CommonJS. The `.cjs` tutorial runs independently of your project's `type` setting. Keep the generated lockfile and install dependencies on the deployment platform so npm selects the matching native package.

`terminateSession()` requests session termination but does not replace `dispose()` for deterministic native cleanup. Once termination is handled, processing is no longer allowed. Disposal is idempotent; other methods fail after disposal. Context handles are independent objects, but a retained context no longer controls a live processor after that processor is disposed.

For voice activity detection (VAD), use `Vad` or `VadAsync` with a dedicated VAD model. Feed original input to VAD before synchronous enhancement modifies it. See the [VAD example](https://github.com/ai-coustics/aic-sdk-node/blob/0.25.0/examples/vad.js). To read speech decisions from the enhancement processor itself, without a second model, use [`getEnergyVadContext()`](/reference/sdk/api/node/enhancement#node-processor-getenergyvadcontext). For Tyto analysis, use `Analyzer`, which buffers input separately from enhancement.

## Handle SDK errors

Synchronous operations throw JavaScript errors; asynchronous operations reject promises. Catch failures around model loading, initialization and processing. The error message distinguishes causes such as invalid credentials, unsupported models and an audio configuration mismatch. Never treat a rejected processing promise as enhanced output.

Correct the cause before retrying: use the exact model ID with `Model.download`, keep input blocks consistent with initialization and check [authentication](/models/get-started/authenticate-apps) when processing is not allowed. SDK construction and `dispose()` are synchronous even when you use `ProcessorAsync`; keep lifecycle work outside the audio callback.


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