Realtime Browser Guide

Build realtime audio tools with Voxis in the browser.

This guide explains how the browser API works, how HTML elements connect to the player, and how to control realtime effects with file inputs, sliders, microphone input, crossfade, and convolution IR.

import { createLockedControl, createVoxisRealtimePlayer, effects }
  from "https://spiralRbx.github.io/voxis/api/voxis-realtime.js";

const player = await createVoxisRealtimePlayer({ container: "#player-host" });

1. What the realtime API is

The public browser API lives in the root api/ folder. It ships:

2. The most important concept

Voxis does not auto-detect HTML IDs by magic. Your JavaScript connects DOM elements to the API.

Example:

So the API handles audio and effects, while your script handles the page wiring.

3. Organize the DOM bridge

A simple pattern is to collect all the page elements in one object. That makes it obvious which HTML nodes your editor depends on.

const $ = (selector) => document.querySelector(selector);

const elements = {
  fileInput: $("#editor-file-input"),
  host: $("#editor-player-host"),
  gainRange: $("#gain-range"),
  chorusMixRange: $("#chorus-mix-range"),
};

Voxis still does not read those IDs by itself. Your script reads them, then calls the player methods with the selected file and slider values.

4. Minimal HTML structure

<div id="player-host"></div>
<input id="editor-file-input" type="file" accept="audio/*">
<script type="module" src="./main.js"></script>

#player-host is where Voxis will create and attach the standard <audio> element when you pass that container to the player.

5. Minimal JavaScript setup

import { createLockedControl, createVoxisRealtimePlayer, effects }
  from "https://spiralRbx.github.io/voxis/api/voxis-realtime.js";

const $ = (selector) => document.querySelector(selector);

const fileInput = $("#editor-file-input");
const playerHost = $("#player-host");

const gainControl = createLockedControl({
  input: "#gain-range",
  output: "#gain-output",
  min: 0.0,
  max: 2.0,
  step: 0.01,
  format: (value) => `${value.toFixed(2)}x`,
});

const player = await createVoxisRealtimePlayer({ container: playerHost });

fileInput.addEventListener("change", async (event) => {
  const [file] = event.target.files || [];
  if (!file) {
    return;
  }
  await player.loadFile(file);
});

6. What createVoxisRealtimePlayer() does

The helper starts the audio engine, loads the processors, loads the shipped WASM module, and creates a standard <audio> element when you pass a container.

The public constructor options are:

7. Apply effects in realtime

Use player.setEffects([...]) with builders from the effects object.

player.setEffects([
  effects.gain({ value: 1.1 }),
  effects.compressor({ thresholdDb: -18, ratio: 3.0, makeupDb: 2.0 }),
  effects.chorus({ mix: 0.35, rateHz: 0.9 }),
  effects.hall_reverb({ decaySeconds: 1.8, mix: 0.2 }),
]);

The builder names are the public browser-side API. They mirror the offline effect names as closely as possible, for example effects.gain(...), effects.compressor(...), effects.output_level(...), effects.noise_reduction(...), and effects.hrtf_simulation(...).

If you want the API itself to enforce stricter app-side limits, pass a limits object inside the effect options. That protects against users changing the DOM range in DevTools and sending a larger value than your page intended.

player.setEffects([
  effects.gain({
    value: gainControl.read(),
    limits: { min: 0.0, max: 2.0 },
  }),
  effects.compressor({
    thresholdDb: thresholdControl.read(),
    ratio: 3.0,
    limits: {
      thresholdDb: { min: -48.0, max: -1.0 },
      ratio: { min: 1.0, max: 12.0 },
    },
  }),
]);

createLockedControl(...) locks the app-side min, max, and step in JavaScript. So even if the HTML is changed in DevTools, the control is normalized again before your effect reads the value.

That is still app-side protection, not total browser security. The project that embeds Voxis is still responsible for the final policy, because the browser runs in the user's own environment.

The shipped WASM module is now also the native source of truth for realtime guardrail metadata. EQ/filter/dynamics parameters are clamped inside the native bridge itself, and the other realtime sections read their public limits from that same WASM metadata before configuration reaches the JavaScript processors.

player.onWarning((warning) => {
  console.warn("[Voxis warning]", warning.message);
});

console.log(player.getNativeRealtimeLimits());

8. Player methods you will use most

9. Realtime sliders and range inputs

This is the pattern you use when you want a real editor instead of fixed presets.

<input id="gain-range" type="range" min="0" max="5" step="0.01" value="1">
<input id="chorus-mix-range" type="range" min="0" max="1" step="0.01" value="0.35">
const gainControl = createLockedControl({
  input: "#gain-range",
  output: "#gain-output",
  min: 0.0,
  max: 2.0,
  step: 0.01,
  format: (value) => `${value.toFixed(2)}x`,
});

const chorusMixControl = createLockedControl({
  input: "#chorus-mix-range",
  min: 0.0,
  max: 1.0,
  step: 0.01,
});

function applyRealtimeEffects() {
  player.setEffects([
    effects.gain({ value: gainControl.read(), limits: { min: 0.0, max: 2.0 } }),
    effects.chorus({ mix: chorusMixControl.read(), rateHz: 0.9 }),
  ]);
}

gainControl.element.addEventListener("input", applyRealtimeEffects);
chorusMixControl.element.addEventListener("input", applyRealtimeEffects);
applyRealtimeEffects();

This is exactly how the API test editor works: the script reads the current range values, rebuilds the effect list, and sends the new state to the player.

10. A complete starter example

<div id="player-host"></div>
<input id="editor-file-input" type="file" accept="audio/*">
<input id="gain-range" type="range" min="0" max="4" step="0.01" value="1">
<output id="gain-output">1.00x</output>
<input id="fft-low-range" type="range" min="20" max="4000" step="1" value="90">
<script type="module">
  import { createLockedControl, createVoxisRealtimePlayer, effects }
    from "https://spiralRbx.github.io/voxis/api/voxis-realtime.js";

  const $ = (selector) => document.querySelector(selector);

  const elements = {
    host: $("#player-host"),
    fileInput: $("#editor-file-input"),
    gainRange: $("#gain-range"),
    fftLowRange: $("#fft-low-range"),
  };

  const player = await createVoxisRealtimePlayer({ container: elements.host });
  const gainControl = createLockedControl({
    input: "#gain-range",
    output: "#gain-output",
    min: 0.0,
    max: 2.0,
    step: 0.01,
    format: (value) => `${value.toFixed(2)}x`,
  });

  function applyEffects() {
    player.setEffects([
      effects.gain({
        value: gainControl.read(),
        limits: { min: 0.0, max: 2.0 },
      }),
      effects.fft_filter({
        lowHz: Number(elements.fftLowRange.value),
        highHz: 9000,
        mix: 1.0,
      }),
    ]);
  }

  elements.fileInput.addEventListener("change", async (event) => {
    const [file] = event.target.files || [];
    if (!file) {
      return;
    }
    await player.loadFile(file);
  });

  gainControl.element.addEventListener("input", applyEffects);
  elements.fftLowRange.addEventListener("input", applyEffects);

  applyEffects();
</script>

If you want a live starter instead of only a code snippet, open realtime-example.html. It ships a working page plus a View code button that reveals the HTML and JavaScript used by the example itself.

11. Use microphone input

await player.useMicrophone();

Stop microphone monitoring with:

player.stopMicrophone();

12. Use a crossfade partner

<input id="crossfade-file-input" type="file" accept="audio/*">
const crossfadeInput = $("#crossfade-file-input");

crossfadeInput.addEventListener("change", async (event) => {
  const [file] = event.target.files || [];
  if (!file) {
    return;
  }
  await player.loadCrossfadePartnerFile(file);
  player.setEffects([
    effects.crossfade({ durationMs: 1200 }),
  ]);
});

13. Use convolution IR

<input id="ir-file-input" type="file" accept="audio/*">
const irInput = $("#ir-file-input");

irInput.addEventListener("change", async (event) => {
  const [file] = event.target.files || [];
  if (!file) {
    player.clearConvolutionIr();
    return;
  }
  await player.loadConvolutionIrFile(file, true);
  player.setEffects([
    effects.convolution_reverb({ mix: 0.28, normalizeIr: true }),
  ]);
});

14. Local files versus GitHub Pages

If you are building inside this repository, you can import from a relative path:

import { createVoxisRealtimePlayer, effects } from "./api/voxis-realtime.js";

If you are using the published GitHub Pages version from another project, use the full URL:

import { createVoxisRealtimePlayer, effects }
  from "https://spiralRbx.github.io/voxis/api/voxis-realtime.js";

15. What the API already covers

The realtime browser API currently exposes builder helpers for checklist sections 1 through 11, including:

16. Where to inspect a working example

The repository also ships a complete live editor in:

That app is a practical reference for wiring file inputs, range inputs, status labels, crossfade, IR loading, and realtime effect updates. For the smallest public starter, use realtime-example.html.