# omnitone: ambisonic spatial audio inside the Web Audio API

> Omnitone is a JavaScript library that decodes ambisonic audio and renders it binaurally in the browser using native Web Audio nodes, with renderers for first order and higher order streams. It is aimed at web and VR teams that need 360 video or audio to sound positioned around the listener without a plugin.

**GoogleChrome/omnitone** — Spatial Audio Rendering on the web.

- Repository: https://github.com/GoogleChrome/omnitone
- Website: https://googlechrome.github.io/omnitone
- Stars: 914 · Forks: 118
- Language: JavaScript
- License: Apache-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/googlechrome-omnitone

## Ambisonic decoding with no plugin and no WASM

Omnitone is an implementation of ambisonic decoding and binaural rendering written in the Web Audio API. The rendering process is powered by the fast native features of the Web Audio API, specifically `GainNode` and `ConvolverNode`, which the README says ensures optimum performance.

That last point is the whole design decision, and it is worth spelling out what it rules out. A binaural renderer normally has to convolve the input signal with an HRIR filter set, per ear, and that is expensive. Doing it with a `ConvolverNode` means the browser's own audio thread does the convolution in native code. The decoder itself, the ambisonic rotation and the matrix work, runs in JavaScript, but the heavy part is left to the platform.

The result is that spatial audio works in a browser tab with no plugin, no WebAssembly runtime and no native helper. For a web VR or 360 video experience, that difference decides whether the feature is possible at all.

The implementation follows the Google spatial media specification, and it uses SADIE's binaural filters. Omnitone also powers the Resonance Audio SDK for web, which is a useful signal about how the library is regarded in production.

The feature list is about two stream formats. First-order ambisonic is a four channel stream, and higher-order ambisonic is supported in 2nd order with 9 channels and 3rd order with 16 channels.

## Two renderers, and the difference between them

The API splits into two renderers, and the choice between them is a content decision.

`FOARenderer` decodes and renders a first-order ambisonic stream of 4 channels. `HOARenderer` decodes and renders higher-order ambisonic streams, which Omnitone supports in 2nd and 3rd order, consisting of 9 and 16 channels respectively.

The higher order format is what carries vertical information. A first-order ambisonic stream describes a sound field as a pressure signal plus three gradient components, which is enough to place a sound horizontally around the listener but leaves the sense of up and down weakly defined. Adding the higher order channels is what makes a sound convincingly above or below you, and what allows the rendering to survive rotation without artefacts.

The setup pattern is the same for both. The first-order example in the README:

```js
// Set up an audio element to feed the ambisonic source audio feed.
const audioElement = document.createElement('audio');
audioElement.src = 'audio-file-foa-acn.wav';

// Create AudioContext, MediaElementSourceNode and FOARenderer.
const audioContext = new AudioContext();
const audioElementSource = audioContext.createMediaElementSource(audioElement);
const foaRenderer = Omnitone.createFOARenderer(audioContext);

// Make connection and start play. Hook up the user input for the playback.
foaRenderer.initialize().then(function() {
  audioElementSource.connect(foaRenderer.input);
  foaRenderer.output.connect(audioContext.destination);

  // This is necessary to activate audio playback out of autoplay block.
  someButton.onclick = () => {
    audioContext.resume();
    audioElement.play();
  };
});
```

The higher-order renderer is created the same way:

```js
// Works the same way as FOARenderer. See the usage above.
const hoaRenderer = Omnitone.createHOARenderer(audioContext);
```

Two details in the first-order example are easy to miss. `initialize()` returns a promise, and the connections are made inside it, because the HRIR filters have to be loaded before the graph is meaningful. And the autoplay block is handled by resuming the `AudioContext` from a user gesture, which is standard Web Audio practice but the reason the example wires a button at all.

## Rotation matrices turn the sound field with your camera

The rotation of the sound field can be linked to a device's orientation sensor or to on-screen camera interaction, and the README explains the mechanism through the renderer's rotation matrix.

The rotation matrix can be updated inside your application's animation loop to rotate the entire sound field. Omnitone supports both 3x3 and 4x4 rotation matrices, and they are column-major:

```js
// Rotation with 3x3 or 4x4 matrix.
renderer.setRotationMatrix3(rotationMatrix3);
renderer.setRotationMatrix4(rotationMatrix4);
```

The 4x4 form exists so it can be fed straight from a Three.js camera:

```js
renderer.setRotationMatrix4(camera.matrixWorld.elements);
```

That is the integration that matters for 360 video. If the camera in a WebGL scene is where the user is looking, then rotating the ambisonic field by the camera's world matrix makes the sound follow the view, and the audio appears anchored in the scene rather than attached to the listener's head.

The column-major convention is the detail to get right. WebGL matrices and Three.js matrices are already column-major, so passing them through directly is correct. A rotation matrix assembled in the other convention produces a sound field that is rotated the wrong way, and the symptom is subtle enough to be easy to mistake for a decoding problem.

One behaviour of version 2.0 matters here. Setter calls made before `initialize()` resolves are preserved automatically, so `setChannelMap()`, `setRotationMatrix3()`, `setRotationMatrix4()`, `setRotationMatrixFromCamera()` and `setRenderingMode()` apply immediately upon renderer construction and persist across HRIR initialization. In earlier versions, a rotation set before the filters finished loading could be lost.

## Rendering modes let you switch off the decoder

The renderer has a mode switch, and it is more useful than it sounds.

`setRenderingMode`, or passing `{renderingMode}` to the renderer constructor, changes the operation of the decoder. The README describes three values:

```js
// Mono or regular multi-channel layouts.
renderer.setRenderingMode('bypass');

// Use ambisonic rendering.
renderer.setRenderingMode('ambisonic');

// Disable encoding completely (audio processing disabled).
renderer.setRenderingMode('off');
```

The `bypass` mode is for when the source is not really ambisonic. A file that is mono or a regular multi-channel layout can be pushed through the same graph without being decoded, which means one code path can carry both a spatial stream and a flat one. The `off` mode disables the processing entirely, and the README gives the motivation directly: saving CPU power by disabling the decoder.

That last case is the interesting one for a production page. An ambisonic decoder running on every frame of a video is real work, and on a page where the user may never enter the 360 view, or where the device is a phone with limited headroom, having a mode that turns the whole thing off is worth having. The same applies to accessibility settings or to a battery saver mode.

The combination with rotation is worth noting too. Because the rendering mode can be changed at runtime and persists across initialization, you can build a page that decides at runtime whether the current content is spatial, without rebuilding the audio graph.

## Three ways to get the library

The README documents three routes, and the choice is mostly about how you build.

The first is a script tag from Google's CDN, which gives you a global `Omnitone` object:

```html
<script
  src="https://www.gstatic.com/external_hosted/omnitone/build/omnitone.min.js">
</script>
<script>
  // `Omnitone` object is loaded and ready.
  const audioContext = new AudioContext();
  const foaRenderer = Omnitone.createFOARenderer(audioContext);
</script>
```

The second is npm, for local development:

```bash
npm install omnitone
```

And Omnitone provides both ES module and UMD/CommonJS entry points for bundlers:

```js
import Omnitone from 'omnitone';
// or: const Omnitone = require('omnitone');

const audioContext = new AudioContext();
const foaRenderer = Omnitone.createFOARenderer(audioContext);
```

The third is a clone, using the library files in `build/`:

```bash
git clone https://github.com/GoogleChrome/omnitone.git
```

The package.json explains the entry points. `main` is `build/omnitone.js`, `module` is `build/omnitone.esm.js`, and the `exports` map pairs an `import` condition with the ESM build and a `require` condition with the UMD build, with `./build/*` exposed as well. The published `files` array is just `build/`, so the package is the compiled output and nothing else.

Version 2.0 also added the SSR and bundler support that this entry point map reflects. The build files are now UMD modules working both as a script global and via `require('omnitone')`, paired with the `module` and `exports` entry points for `import Omnitone from 'omnitone'`, plus browser global guards for Node and SSR environments.

For development, the scripts are `build` with rollup, `build-hrir` which runs `src/resources/build-hrir-base64.js` to regenerate the embedded filter data, `build-doc` with jsdoc, `eslint` over `src/` and `test/`, and `test` which runs `web-test-runner` after a build.

## What 2.0 actually changed beyond the version number

The What's New section for 2.0 is the most informative part of the README, because it describes a decade of accumulated fixes in one place.

The change with the widest reach is CSP compatibility without `'unsafe-eval'`. `BufferList.load()` was rewritten around `Promise.all` and `new Function()` was removed, so Omnitone now runs under strict Content Security Policies. For anyone deploying to a site with a strict policy, that is the difference between the library working and not working, and it also removes a construct that many security reviews flag on sight.

Second, promise rejection handling. Load and decode failures in `BufferList`, `FOARenderer` and `HOARenderer` now reject the returned promise cleanly, without throwing uncaught exceptions or leaking duplicate `unhandledrejection` events from `decodeAudioData()`. That is a specific and unglamorous class of bug, and the fact that it was fixed at all suggests it was hard to diagnose from the outside.

Third, correctness fixes. Destination indices and sign mapping in `HOARotator.prototype.getRotationMatrix3()` were fixed, and `Omnitone.splitBufferbyChannel` was renamed to `Omnitone.splitBufferByChannel`, which also fixed an internal `bufflerList` reference error in `Utils.mergeBufferListByChannel`.

Finally, the toolchain was modernised: Node `>=20`, Rollup 4, `@web/test-runner` running headless Chrome, ESLint 8 and GitHub Actions CI. The package.json reflects this, with `engines` set to `node >=20` and the devDependencies pinned to those versions, and `prepublishOnly` running eslint and the test suite before publication.

## Apache-2.0 terms, a long gap, and the Resonance alternative

The licence field on the repository says Apache-2.0, the package.json agrees, and there is a LICENSE file at the top level. Apache-2.0 is the standard choice for a Google Chrome project meant to be embedded in other people's web applications, since it requires only that the notice be preserved and includes an explicit patent grant.

The release history shows a project that spent a long time quiet and then did a substantial amount of work at once. The tags are 1.2.5 on 2018-09-10, 1.3.0 on 2019-01-18, and then v2.0.0 on 2026-09-30, with the last push on 2026-10-04. So there is a gap of more than seven years between 1.3.0 and 2.0.0, and 2.0.0 is a major version with a substantial rewrite behind it.

That gap explains some of the changes in 2.0. A library that had not been updated for years accumulates the kind of problems that only surface once someone tries to deploy it under a modern build, which is exactly what the CSP work, the SSR entry points and the promise handling look like.

For the alternative, the README itself points you to it. If you are looking for interactive panning based on Omnitone's ambisonic rendering, it directs you to the Resonance Audio Web SDK project. The distinction is in what the two do with a sound field. Omnitone plays an ambisonic stream as it was recorded, optionally rotating it to follow the view, which is what a 360 video needs. Interactive panning means taking a sound and moving it around the listener in response to input, and that is a different job with a different API.

For teams that need to author ambisonic content rather than decode it, Omnitone is the wrong tool by construction, because the README documents decoding and rendering only, and the encoding side belongs to the spatial media tooling.

## Conclusion

Adopt omnitone when your content is ambisonic, a 360 video or 360 audio file, and you want a Web Audio graph that plays it positioned around the listener, either static or rotated with the camera. Skip it if you want interactive panning of a mono or stereo source, since the README points to the Resonance Audio Web SDK for that, and skip it if you need an authoring or encoding toolchain, because Omnitone only decodes and renders. Verify first that your content matches the order of the stream, first order for four channels, second or third order for nine or sixteen, and that the rotation matrix convention matches your camera, since the matrices are column-major. The licence field on the repository says Apache-2.0, the last push was on 2026-10-04, and the current release is v2.0.0.

## FAQ

### What is Omnitone and what does it do?

Omnitone is an implementation of ambisonic decoding and binaural rendering written in the Web Audio API, powered by the native `GainNode` and `ConvolverNode`. It renders first-order ambisonic streams of 4 channels and higher-order streams of 9 and 16 channels, and it is based on the Google spatial media specification with SADIE's binaural filters.

### How do I install Omnitone?

You can load it from Google's CDN with a script tag pointing at `https://www.gstatic.com/external_hosted/omnitone/build/omnitone.min.js`, install it with `npm install omnitone`, or clone the repository and use the library files in `build/`. The package exposes ES module and UMD/CommonJS entry points.

### What is the difference between FOARenderer and HOARenderer?

`FOARenderer` decodes and renders a first-order ambisonic stream of 4 channels, while `HOARenderer` handles higher-order ambisonics in 2nd and 3rd order, which are 9 and 16 channels respectively. The setup is the same, and the higher order formats carry the vertical information a first-order stream lacks.

### How do I rotate the sound field with a Three.js camera?

Call `renderer.setRotationMatrix4(camera.matrixWorld.elements)`, updating it inside your animation loop. Omnitone supports 3x3 and 4x4 rotation matrices in column-major form, which matches Three.js matrix conventions, and setter calls made before `initialize()` resolves are preserved.

### What do the omnitone rendering modes do?

Use `setRenderingMode` or pass `renderingMode` to the constructor. `bypass` handles mono or regular multi-channel layouts without decoding, `ambisonic` enables ambisonic rendering, and `off` disables the processing entirely, which the README describes as a way to save CPU power.

### What licence is Omnitone released under and when was it last updated?

The licence field on the repository says Apache-2.0, with the same declaration in package.json and a LICENSE file at the top level. The releases are 1.2.5 on 2018-09-10, 1.3.0 on 2019-01-18 and v2.0.0 on 2026-09-30, and the last push was on 2026-10-04.

## Sources

- [GoogleChrome/omnitone on GitHub](https://github.com/GoogleChrome/omnitone)
- [License: Apache-2.0](https://github.com/GoogleChrome/omnitone/blob/master/LICENSE)
- [Project website](https://googlechrome.github.io/omnitone)
- [README](https://github.com/GoogleChrome/omnitone/blob/master/README.md)
- [Releases](https://github.com/GoogleChrome/omnitone/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/googlechrome-omnitone
