tunelet

Documentation

Getting started

Overview

tunelet is a vocal processor: pitch correction, choir and harmony voices, formant control and effects in one deterministic signal chain.

The core is a dependency-free Rust crate that forbids unsafe code. The same crate compiles to WebAssembly with TypeScript declarations, so browsers, Node.js and native hosts run the same fixed-point signal chain and give consistent results.

  • Input: mono signed 16-bit PCM at 44.1 kHz or 48 kHz.
  • Output: planar stereo, interleaved stereo frames (Rust) or an in-place mono downmix.
  • Latency: 1,593 frames of nominal resynthesis delay at both rates.
  • Runtime: 32-bit fixed point. No heap allocation, locks or I/O while processing.

If you are new, read Install and Quick start, then Signal chain and Buffers and blocks. Everything else can be read in any order.

Install

Both packages are published: tunelet on npm and tunelet on crates.io.

The npm package is one ES module that bundles the Wasm binary, the generated bindings and the shared loadTunelet entry point. The same import works in browsers and in Node.js.

npm install tunelet
# or
pnpm add tunelet
yarn add tunelet

For native Rust, add the crate. The default build has no dependencies and uses only safe Rust. The wasm feature is only for wasm32-unknown-unknown builds.

cargo add tunelet

The package exposes only ESM exports. CommonJS applications can use await import("tunelet"). The tunelet/wasm subpath exposes the raw generated bindings, including initSync.

Quick start

Five calls take you from an empty project to processed audio.

  1. Call await loadTunelet() once. The import of the package does not load the binary.
  2. Build a configuration from defaultConfig(), or omit it to use the defaults.
  3. Construct new Tunelet(sampleRate, maxBlockFrames, config). Both numbers are fixed for the lifetime of the processor.
  4. Allocate Int16Array buffers once and call process(input, left, right) for every block.
  5. Call free() when the processor is no longer needed.
quick-start.ts
import { function loadTunelet(source?: InitInput | Promise<InitInput>): Promise<InitOutput>

Initializes the Wasm module one time for this imported package instance.

If you give no argument, the function finds the packaged binary relative to this module. Node.js reads a file: URL, and browsers fetch the served asset. An import of the package does not load Wasm. In browsers, serve the assets over HTTP(S), or give the bytes or a compiled module as the source.

For a custom deployment, you can give one of these sources:

  • A URL.
  • A fetch Response.
  • A byte buffer (BufferSource).
  • A compiled WebAssembly.Module.
  • A promise for one of these.

A string source obeys fetch rules. For a Node.js file: source, use a URL object. A compiled module must use imports that agree with the packaged bindings.

Concurrent calls share one promise, and the first call selects the source. After a successful initialization, later calls use it again and do not load Wasm again, whatever their arguments are. If an attempt fails, the promise rejects and the function clears the cached promise. Then you can try again. File, network and Wasm errors go to the caller. The function does not try a different load method after an error.

@paramsource The optional Wasm source. The default is the binary in this package.@returnsThe initialized Wasm exports. Use the re-exported Tunelet class for processors.
loadTunelet
, class Tunelet

A stateful PCM processor that JavaScript uses as the Tunelet class.

The input is signed 16-bit mono PCM. The output is planar stereo or an in-place mono downmix. The quantization, latency and configuration behavior are the same as in the native processor. Buffer lengths count frames, not bytes.

These rules apply to the buffers:

  • Separate stereo output arrays must not overlap.
  • The input can overlap an output, because the class copies the input first.
  • All buffers must stay attached until each call is complete.

If a call that processes audio fails, it throws a JavaScript Error before the audio state or the output changes. The class rejects malformed configuration objects and malformed event objects. It also rejects values that are out of range. The class decodes objects inside the methods that can fail. Thus, if a conversion fails, Rust releases the temporary values as usual.

Tunelet
, function defaultConfig(): ProcessorConfig

Returns a complete, independent default configuration for JavaScript.

Change its fields and give it to Tunelet or to setConfig. The randomSeed field is a JavaScript bigint, so it keeps all 64 bits. After the binding decodes them, floating-point controls have the precision of the Rust f32 type.

Errors

If the conversion to a JavaScript object fails, this function returns a JavaScript Error.

defaultConfig
} from "tunelet";
// 1. Load the Wasm module once per process. await function loadTunelet(source?: InitInput | Promise<InitInput>): Promise<InitOutput>

Initializes the Wasm module one time for this imported package instance.

If you give no argument, the function finds the packaged binary relative to this module. Node.js reads a file: URL, and browsers fetch the served asset. An import of the package does not load Wasm. In browsers, serve the assets over HTTP(S), or give the bytes or a compiled module as the source.

For a custom deployment, you can give one of these sources:

  • A URL.
  • A fetch Response.
  • A byte buffer (BufferSource).
  • A compiled WebAssembly.Module.
  • A promise for one of these.

A string source obeys fetch rules. For a Node.js file: source, use a URL object. A compiled module must use imports that agree with the packaged bindings.

Concurrent calls share one promise, and the first call selects the source. After a successful initialization, later calls use it again and do not load Wasm again, whatever their arguments are. If an attempt fails, the promise rejects and the function clears the cached promise. Then you can try again. File, network and Wasm errors go to the caller. The function does not try a different load method after an error.

@paramsource The optional Wasm source. The default is the binary in this package.@returnsThe initialized Wasm exports. Use the re-exported Tunelet class for processors.
loadTunelet
();
// 2. Start from the defaults and change what you need. const const config: ProcessorConfigconfig = function defaultConfig(): ProcessorConfig

Returns a complete, independent default configuration for JavaScript.

Change its fields and give it to Tunelet or to setConfig. The randomSeed field is a JavaScript bigint, so it keeps all 64 bits. After the binding decodes them, floating-point controls have the precision of the Rust f32 type.

Errors

If the conversion to a JavaScript object fails, this function returns a JavaScript Error.

defaultConfig
();
const config: ProcessorConfigconfig.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.pitchRatio: number

The pitch multiplier, 0.25..=4. The default is 1.

A value of 0.5 lowers the pitch by one octave. A value of 2 raises the pitch by one octave. This value multiplies the correction ratio and the ratio of each voice. A value above 1 also enables the anti-alias low-pass filters.

pitchRatio
= 1.25;
// 3. Prepare a processor: sample rate, block limit, configuration. const const engine: Tuneletengine = new new Tunelet(sample_rate: number, max_block_frames: number, config?: ProcessorConfig | null): Tunelet

Prepares a processor and its reusable PCM buffers.

sample_rate must be 44100 or 48000. max_block_frames must be an integer in 1..=4294967295. It is a limit for each call, and smaller blocks and empty blocks are also valid. If you omit config, the processor uses the default configuration. The constructor validates the settings before it allocates the processor or the PCM buffers. In TypeScript, use new Tunelet(sampleRate, maxBlockFrames, config).

Errors

The constructor throws if one of these conditions occurs:

  • The rate is not 44100 or 48000.
  • The block limit is not an integer or is out of range.
  • The configuration object is malformed or a field is missing.
  • The configuration is not valid.
  • The reservation of a PCM buffer fails.

The available Wasm memory must also have sufficient space for the DSP histories.

Tunelet
(48000, 1024, const config: ProcessorConfigconfig);
// 4. Allocate the PCM buffers once and reuse them for every block. const const input: Int16Array<ArrayBuffer>input = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(1024);
const const left: Int16Array<ArrayBuffer>left = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(1024);
const const right: Int16Array<ArrayBuffer>right = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(1024);
// Fill input with mono PCM, then render one block of stereo output. const engine: Tuneletengine.Tunelet.process(input: Int16Array, left: Int16Array, right: Int16Array, events?: BlockEvents | null): void

Processes mono input into left and right Int16Array buffers of equal length.

If the call is successful, it writes every output sample. Optional events apply before their frame, and their offsets obey the rules of TimedEvent[]. An empty block must have no events. A successful call cancels a drain, also for an empty block. The call uses the existing JavaScript and Rust PCM buffers again.

Errors

The method throws if the channel lengths are not equal. It also throws if the block is larger than maxBlockFrames or an event is not valid. The call checks the lengths and the block size before it decodes and validates the events. If an error occurs, the output buffers and the DSP state do not change.

process
(const input: Int16Array<ArrayBuffer>input, const left: Int16Array<ArrayBuffer>left, const right: Int16Array<ArrayBuffer>right);
// 5. Release the Wasm memory when you are done. const engine: Tuneletengine.Tunelet.free(): voidfree();

The native API mirrors the Wasm one. PrepareConfig fixes the sample rate and the block limit, ProcessorConfig holds the settings, and Processor::new allocates everything up front.

main.rs
use tunelet::{PrepareConfig, Processor, ProcessorConfig, SampleRate};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // The sample rate and the block limit are fixed at construction.
    let prep = PrepareConfig::new(SampleRate::Hz48000, 1024)?;
    let mut config = ProcessorConfig::default();
    config.resynthesis.pitch_ratio = 1.25;

    let mut processor = Processor::new(prep, config)?;

    let input = [0i16; 1024];
    let mut left = [0i16; 1024];
    let mut right = [0i16; 1024];

    // No allocation, locks or I/O happen inside this call.
    processor.process_planar(&input, &mut left, &mut right, &[])?;
    Ok(())
}

The default configuration is not a bypass. It already performs chromatic pitch correction with one choir voice and adds the full resynthesis latency.

At 48 kHz, tunelet 0.1.1 builds the default chromatic note table from 44.1 kHz periods. Thus, the default correction gives the wrong pitch. To load the table for the real rate, set correction.enabled to false and then to true with two setConfig calls. Or use a non-chromatic notes mask. 44.1 kHz has no problem.

Concepts

Signal chain

Every block passes through six stages in a fixed order.

StageConfig keyWhat it doesDefault
PunchpunchInput gain, transient emphasis and arctangent saturationoff
WarmwarmAsymmetric saturation followed by a 50 Hz high-passoff
ResynthesisresynthesisPitch tracking, correction, pitch shift, choir or harmony voices, formantschromatic correction, one voice
AlienalienReverse playback of recent audio in fading loopsoff
ReverbreverbEcho and reverberation networksoff
Output gainoutputGainLinear gain 0..=64, then clipping to 16 bits1

Resynthesis changes the pitch. The tracker finds the period in two stages. An LPC model captures the vocal tract. The engine builds the voices again: it reads the history buffer with period jumps. The homepage explains each of these principles.

Disabled stages do not touch the audio, but their settings are still validated. Alien is an exception: its recording history continues to advance while Alien is bypassed. Thus, when you enable Alien, it can play audio that it recorded earlier.

Buffers and blocks

All audio is signed 16-bit PCM, and every call must fit the block limit chosen at construction.

  • The sample rate is 44,100 or 48,000 Hz. Create Web Audio contexts with one of these rates.
  • maxBlockFrames is a per-call limit. Shorter blocks and empty blocks are valid.
  • Left and right output arrays must have equal length and must not overlap. The input can overlap an output because the processor copies it first.
  • Buffers must stay attached for the duration of the call. The same rules apply to slices in Rust.
  • Web Audio works in Float32. Scale to [-32768, 32767] before process() and divide by 32768 afterwards.
blocks.ts
const const engine: Tuneletengine = new new Tunelet(sample_rate: number, max_block_frames: number, config?: ProcessorConfig | null): Tunelet

Prepares a processor and its reusable PCM buffers.

sample_rate must be 44100 or 48000. max_block_frames must be an integer in 1..=4294967295. It is a limit for each call, and smaller blocks and empty blocks are also valid. If you omit config, the processor uses the default configuration. The constructor validates the settings before it allocates the processor or the PCM buffers. In TypeScript, use new Tunelet(sampleRate, maxBlockFrames, config).

Errors

The constructor throws if one of these conditions occurs:

  • The rate is not 44100 or 48000.
  • The block limit is not an integer or is out of range.
  • The configuration object is malformed or a field is missing.
  • The configuration is not valid.
  • The reservation of a PCM buffer fails.

The available Wasm memory must also have sufficient space for the DSP histories.

Tunelet
(48000, 2048);
// Any block up to maxBlockFrames is valid, including short and empty blocks. const const frames: 480frames = 480; const engine: Tuneletengine.Tunelet.process(input: Int16Array, left: Int16Array, right: Int16Array, events?: BlockEvents | null): void

Processes mono input into left and right Int16Array buffers of equal length.

If the call is successful, it writes every output sample. Optional events apply before their frame, and their offsets obey the rules of TimedEvent[]. An empty block must have no events. A successful call cancels a drain, also for an empty block. The call uses the existing JavaScript and Rust PCM buffers again.

Errors

The method throws if the channel lengths are not equal. It also throws if the block is larger than maxBlockFrames or an event is not valid. The call checks the lengths and the block size before it decodes and validates the events. If an error occurs, the output buffers and the DSP state do not change.

process
(const input: Int16Array<ArrayBuffer>input.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, const frames: 480frames), const left: Int16Array<ArrayBuffer>left.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, const frames: 480frames), const right: Int16Array<ArrayBuffer>right.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, const frames: 480frames));
// In-place mono downmix: the input array receives the output. const engine: Tuneletengine.Tunelet.processMono(audio: Int16Array, events?: BlockEvents | null): void

Replaces the audio in a mono Int16Array with the stereo downmix.

The timing, the events and the drain cancellation are the same as for process. Peak feedback measures the stereo output before the downmix, as in the native processor.

Errors

The method throws if the block is too large or an event is not valid. It throws before the audio of the caller or the DSP state changes.

processMono
(const mono: Int16Array<ArrayBuffer>mono.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, const frames: 480frames));
var console: Consoleconsole.Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(const engine: Tuneletengine.Tunelet.sampleRate: number

The sample rate of the input and the output in hertz.

The constructor sets this value, and the value does not change.

sampleRate
, const engine: Tuneletengine.Tunelet.maxBlockFrames: number

The maximum frame count for each call that processes or drains audio.

The constructor sets this value, and the value does not change.

maxBlockFrames
, const engine: Tuneletengine.Tunelet.latencyFrames: number

The nominal resynthesis delay in frames.

Other signal paths have different timing. See the latencyFrames property for the timing of the dry path and the voices.

latencyFrames
);
Output layoutProcessTail
Planar left and rightprocess / process_planardrain / drain_planar
Interleaved [[i16; 2]] frames (Rust)processdrain
Mono, in placeprocessMono / process_mono_in_placedrainMono / drain_mono

Latency and draining

Resynthesis looks 1,593 frames ahead. Flush that window when the input ends.

latencyFrames reports 1,593 frames at both sample rates, which is 33.2 ms at 48 kHz and 36.1 ms at 44.1 kHz. The dry path inside resynthesis is delayed by 1,543 frames, and shifted voices and effects have their own timing, so treat the figure as nominal.

  1. After the last input block, call beginDrain() once. It returns the tail budget in frames. tailFrames() reports the same budget for the current configuration.
  2. Call drain(left, right) in a loop. Each call returns how many frames it rendered and zero-fills the rest. Use only the returned frames.
  3. Stop when a call returns 0. Completion resets the audio histories and the feedback.
drain.ts
// After the last input block, start the finite tail budget once.
const const budget: numberbudget = const engine: Tuneletengine.Tunelet.beginDrain(): number

Starts a finite tail budget and returns its frame count as a number.

Call this method one time after the final input block. Another call starts the full budget again, but it does not clear the histories. See beginDrain for completion.

beginDrain
();
while (true) { const const frames: numberframes = const engine: Tuneletengine.Tunelet.drain(left: Int16Array, right: Int16Array): number

Renders stereo tail frames into Int16Array buffers of equal length.

The method returns the number of rendered frames and fills the unused output with zeros. If the drain is not active or is complete, the method returns zero. Empty arrays do not use the budget. Completion resets the histories and the feedback in the final drain call, also when that call returns audio.

Errors

The method throws if the channel lengths are not equal or if a block is larger than maxBlockFrames. After an error, both output arrays and the drain state do not change.

drain
(const left: Int16Array<ArrayBuffer>left, const right: Int16Array<ArrayBuffer>right);
if (const frames: numberframes === 0) break; // Use only left.subarray(0, frames) and right.subarray(0, frames). }

A successful process() or setConfig() cancels an active drain, and so does reset(). Empty drain blocks do not consume the budget.

Determinism and safety

The same input and settings give the same output wherever the engine runs, and nothing in the audio path allocates.

  • Signal processing uses 32-bit fixed-point arithmetic with explicit wrapping and quantization, so results do not depend on the host’s floating-point behavior.
  • Processing, configuration, MIDI, feedback, reset and drain do not allocate, acquire locks or perform I/O. Construction is the only allocation.
  • The crate forbids unsafe and has no dependencies in its default build.
  • Every call validates its buffers, configuration and events before it mutates state. A failing call leaves the processor and the output untouched.

The Wasm binding copies audio across the JavaScript boundary and can allocate when it converts configuration objects, events, errors and feedback. It is suitable for AudioWorklets and offline rendering, but it does not promise hard real-time scheduling the way the native crate does.

Control

Configuration

One ProcessorConfig object describes the whole chain. Edit a copy, then apply it atomically.

  1. Start from defaultConfig(), or from config(), which returns an independent copy of the active settings.
  2. Change the fields you need. Objects use camelCase names and enums are string literals, so your editor completes them.
  3. Apply with setConfig(config), with the constructor, or with a timed configure event.
config.ts
// Snapshot, edit, apply. The whole object is validated before anything changes.
const const cfg: ProcessorConfigcfg = const engine: Tuneletengine.Tunelet.config(): ProcessorConfig

Returns a copy of the active configuration as a new JavaScript object.

A change to the object has no effect until setConfig or a timed event applies it.

Errors

If the conversion to a JavaScript object fails, this method throws.

config
();
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.pitchRatio: number

The pitch multiplier, 0.25..=4. The default is 1.

A value of 0.5 lowers the pitch by one octave. A value of 2 raises the pitch by one octave. This value multiplies the correction ratio and the ratio of each voice. A value above 1 also enables the anti-alias low-pass filters.

pitchRatio
= 0.5; // one octave down
const cfg: ProcessorConfigcfg.ProcessorConfig.outputGain: number

The linear gain after all effects, 0..=64.

The default is 1 (unity). Zero mutes the output, but the internal processing continues. The gain is quantized in steps of 1/256 before the fixed-point multiplication and the final PCM clipping. Thus, a positive value below 1/256 also mutes the output.

outputGain
= 0.8;
const engine: Tuneletengine.Tunelet.setConfig(config: ProcessorConfig): void

Decodes and validates a configuration, then replaces the complete configuration with it.

State retention and drain cancellation follow the native processor. The conversion ignores additional properties of the JavaScript object.

Errors

The method throws if an object is not valid or a field is missing. After an error, all processor state and any active drain stay the same.

setConfig
(const cfg: ProcessorConfigcfg);
// An out-of-range value throws, and the processor keeps its previous settings. try { const cfg: ProcessorConfigcfg.ProcessorConfig.outputGain: number

The linear gain after all effects, 0..=64.

The default is 1 (unity). Zero mutes the output, but the internal processing continues. The gain is quantized in steps of 1/256 before the fixed-point multiplication and the final PCM clipping. Thus, a positive value below 1/256 also mutes the output.

outputGain
= 100; // the valid range is 0..=64
const engine: Tuneletengine.Tunelet.setConfig(config: ProcessorConfig): void

Decodes and validates a configuration, then replaces the complete configuration with it.

State retention and drain cancellation follow the native processor. The conversion ignores additional properties of the JavaScript object.

Errors

The method throws if an object is not valid or a field is missing. After an error, all processor state and any active drain stay the same.

setConfig
(const cfg: ProcessorConfigcfg);
} catch (var error: unknownerror) { var console: Consoleconsole.Console.error(...data: any[]): void

The console.error() static method outputs a message to the console at the 'error' log level.

MDN Reference

error
(var error: unknownerror);
}

Validation covers every field, including stages that are disabled. A missing field or an out-of-range value throws an Error in JavaScript and returns Err in Rust. Extra properties are ignored. randomSeed is a bigint.

GroupFields worth knowingRange
resynthesispitchRatio0.25..=4 (0.5 is one octave down)
resynthesis.correctionenabled, notes, retuneMs12-entry mask C..B; 0..=400 ms half-life
resynthesis.formantwidth, stretch, aspirationMix0.5..=2, 0.5..=2, 0..=1
resynthesismode, choirVoices, harmony.*"choir" or "harmony"; 1..=16 voices
punch, warmgainDb, impact, driveDb-96..=12 dB, 0..=1, 0..=6 dB
reverbmode, mix, diffusion, decay, brightnessHzsix networks; 0..=1; 80..=20000 Hz
outputGain0..=64, linear

The generated declarations carry the full parameter documentation from the Rust types, including defaults and side effects. The API reference lists every field.

Common setups

Four configurations that cover most use cases. Combine them freely.

Pitch correction to a scale. The note mask lists the twelve pitch classes from C to B. retuneMs is a half-life. With zero, the pitch goes to the target at the next tracking update. Larger values glide.

pitch-correction.ts
const const cfg: ProcessorConfigcfg = const engine: Tuneletengine.Tunelet.config(): ProcessorConfig

Returns a copy of the active configuration as a new JavaScript object.

A change to the object has no effect until setConfig or a timed event applies it.

Errors

If the conversion to a JavaScript object fails, this method throws.

config
();
// Snap to D major. The mask runs C, C#, D, D#, E, F, F#, G, G#, A, A#, B. const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.correction: CorrectionConfig

The target notes and the retune time. The default is enabled chromatic correction with a retune time of zero.

correction
.CorrectionConfig.enabled: boolean

Enables the selection of correction targets. The default is true.

If you disable correction, the ratio moves toward 1.0 with retune_ms at the next tracking updates. The ratio does not change to 1.0 immediately.

enabled
= true;
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.correction: CorrectionConfig

The target notes and the retune time. The default is enabled chromatic correction with a retune time of zero.

correction
.CorrectionConfig.notes: NoteMask

The configured target pitch classes. The default is [NoteMask::CHROMATIC].

All boolean masks are valid. MIDI can replace the active table. This replacement stays until this mask or enabled changes.

notes
= [
false, true, true, false, true, false, true, true, false, true, false, true, ]; // Half-life of the glide toward the target, 0..=400 ms. Zero snaps at once. const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.correction: CorrectionConfig

The target notes and the retune time. The default is enabled chromatic correction with a retune time of zero.

correction
.CorrectionConfig.retuneMs: number

The half-life of the smoothing in milliseconds, 0..=400. The default is 0.

If the value is zero, the ratio changes to the target at the next tracking update. A value that is not zero is a half-life. It is not a fixed time to get exactly to the target.

retuneMs
= 35;
const engine: Tuneletengine.Tunelet.setConfig(config: ProcessorConfig): void

Decodes and validates a configuration, then replaces the complete configuration with it.

State retention and drain cancellation follow the native processor. The conversion ignores additional properties of the JavaScript object.

Errors

The method throws if an object is not valid or a field is missing. After an error, all processor state and any active drain stay the same.

setConfig
(const cfg: ProcessorConfigcfg);

Harmony and choir. Harmony renders up to four extra voices from the tracked input, with fixed intervals, scale degrees, named chords or MIDI. Choir layers detuned copies of the lead. Voices are summed, so more voices raise the level.

harmony.ts
const const cfg: ProcessorConfigcfg = const engine: Tuneletengine.Tunelet.config(): ProcessorConfig

Returns a copy of the active configuration as a new JavaScript object.

A change to the object has no effect until setConfig or a timed event applies it.

Errors

If the conversion to a JavaScript object fails, this method throws.

config
();
// Four harmony voices at fixed intervals around the lead. const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.mode: MultiVoiceMode

The voice arrangement. The default is [MultiVoiceMode::Choir].

Harmony renders the lead voice and a maximum of four harmony voices.

mode
= "harmony";
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.harmony: HarmonyConfig

The controls for the four harmony voices. The default is MIDI-channel allocation.

Validation applies to these controls, and they can receive MIDI, also while Choir is selected.

harmony
.HarmonyConfig.source: HarmonySource

The pitch source. The default is [HarmonySource::MidiChannels].

source
= "fixedInterval";
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.harmony: HarmonyConfig

The controls for the four harmony voices. The default is MIDI-channel allocation.

Validation applies to these controls, and they can receive MIDI, also while Choir is selected.

harmony
.HarmonyConfig.chromaticIntervals: [number, number, number, number]

Four fixed semitone offsets, each in -24..=24. The default is all zeros. [HarmonySource::FixedInterval] uses them. A value of 12 makes the pitch one octave higher.

chromaticIntervals
= [4, 7, -5, 12];
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.harmonyGain: number

The pan gain of the harmony voices, 0..=1. The default is 0.5.

The code applies this gain only if stereo_output is true. The harmony path without pan does not use this control. It does not scale the lead voice.

harmonyGain
= 0.6;
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.stereoOutput: boolean

Enables a stereo pan for each voice. The default is false.

This value is independent of the buffer layout of the process method. A change to this value resets some audio histories and the feedback. [crate::Processor::set_config] gives the details.

stereoOutput
= true;
const engine: Tuneletengine.Tunelet.setConfig(config: ProcessorConfig): void

Decodes and validates a configuration, then replaces the complete configuration with it.

State retention and drain cancellation follow the native processor. The conversion ignores additional properties of the JavaScript object.

Errors

The method throws if an object is not valid or a field is missing. After an error, all processor state and any active drain stay the same.

setConfig
(const cfg: ProcessorConfigcfg);
// Or a thick unison choir instead. const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.mode: MultiVoiceMode

The voice arrangement. The default is [MultiVoiceMode::Choir].

Harmony renders the lead voice and a maximum of four harmony voices.

mode
= "choir";
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.choirVoices: number

The total number of choir voices, with the lead voice, 1..=16. The default is 1.

The other voices read the delayed lead history with random pitch drift. The code adds the voices and does not divide the sum by the number of voices. Thus, more voices can increase the output level and the clipping. Harmony does not use this value.

choirVoices
= 6;
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.choirDetune: number

The drift control for secondary voices, 10..=100, without a unit. The default is 50.

The unit is not cents. The lead voice has no random choir drift. Thus, a choir with one voice does not get this detune.

choirDetune
= 40;
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.choirSpreadPercent: number

The stereo width of the choir, 0..=100%. The default is 100%.

The code uses this value only if stereo_output enables the voice pan and the choir has more than one voice.

choirSpreadPercent
= 80;
const engine: Tuneletengine.Tunelet.setConfig(config: ProcessorConfig): void

Decodes and validates a configuration, then replaces the complete configuration with it.

State retention and drain cancellation follow the native processor. The conversion ignores additional properties of the JavaScript object.

Errors

The method throws if an object is not valid or a field is missing. After an error, all processor state and any active drain stay the same.

setConfig
(const cfg: ProcessorConfigcfg);

Formant control. With formants enabled, the engine models the vocal tract separately, so a large pitch shift no longer sounds like a chipmunk. width and stretch reshape the tract. Aspiration adds breath noise that follows the signal level.

formant.ts
const const cfg: ProcessorConfigcfg = const engine: Tuneletengine.Tunelet.config(): ProcessorConfig

Returns a copy of the active configuration as a new JavaScript object.

A change to the object has no effect until setConfig or a timed event applies it.

Errors

If the conversion to a JavaScript object fails, this method throws.

config
();
// Keep the vocal tract where it is while the pitch moves one octave up. const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.pitchRatio: number

The pitch multiplier, 0.25..=4. The default is 1.

A value of 0.5 lowers the pitch by one octave. A value of 2 raises the pitch by one octave. This value multiplies the correction ratio and the ratio of each voice. A value above 1 also enables the anti-alias low-pass filters.

pitchRatio
= 2;
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.formant: FormantConfig

The vocal-tract changes and aspiration. They are disabled by default.

formant
.FormantConfig.enabled: boolean

Enables formant analysis and synthesis. The default is false.

enabled
= true;
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.formant: FormantConfig

The vocal-tract changes and aspiration. They are disabled by default.

formant
.FormantConfig.width: number

The vocal-tract width factor, 0.5..=2. The default is 1.

The square of the width scales the estimated tract areas. The width also scales the amplitude of the synthesized output.

width
= 1; // 0.5..=2, scales the tract area
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.formant: FormantConfig

The vocal-tract changes and aspiration. They are disabled by default.

formant
.FormantConfig.stretch: number

The vocal-tract length factor, 0.5..=2. The default is 1.

It controls the interpolation of the area model and thus the filter order.

stretch
= 1; // 0.5..=2, scales the tract length
// Optional breathiness that follows the signal level. const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.formant: FormantConfig

The vocal-tract changes and aspiration. They are disabled by default.

formant
.FormantConfig.aspirationMix: number

The aspiration level, 0..=1, without a unit. The default is 0.

If the value is 0.01 or less, the stage makes no noise. Active aspiration follows the signal amplitude. It uses the random stream of the processor, together with other stages. During each drain call, the processor sets the aspiration to zero. The configuration that the host sees does not change.

aspirationMix
= 0.15;
const cfg: ProcessorConfigcfg.ProcessorConfig.resynthesis: ResynthesisConfig

The stage for pitch tracking, correction, voice resynthesis and formant control.

The default is [ResynthesisConfig::default].

resynthesis
.ResynthesisConfig.formant: FormantConfig

The vocal-tract changes and aspiration. They are disabled by default.

formant
.FormantConfig.aspirationCutoffHz: number

The high-pass cutoff for aspiration in hertz, 500..=10000. The default is 4000.

The stage limits the cutoff to 0.48 × sample rate and truncates it to whole hertz. A change to this value updates the coefficients. The filter history does not change.

aspirationCutoffHz
= 4000;
const engine: Tuneletengine.Tunelet.setConfig(config: ProcessorConfig): void

Decodes and validates a configuration, then replaces the complete configuration with it.

State retention and drain cancellation follow the native processor. The conversion ignores additional properties of the JavaScript object.

Errors

The method throws if an object is not valid or a field is missing. After an error, all processor state and any active drain stay the same.

setConfig
(const cfg: ProcessorConfigcfg);

Effects. Punch and Warm shape the input before pitch tracking. Alien and Reverb then color the result.

effects.ts
const const cfg: ProcessorConfigcfg = const engine: Tuneletengine.Tunelet.config(): ProcessorConfig

Returns a copy of the active configuration as a new JavaScript object.

A change to the object has no effect until setConfig or a timed event applies it.

Errors

If the conversion to a JavaScript object fails, this method throws.

config
();
const cfg: ProcessorConfigcfg.ProcessorConfig.punch: PunchConfig

The input dynamics and saturation stage, which applies first.

It is disabled by default.

punch
.PunchConfig.enabled: boolean

Enables this input stage. The default is false.

enabled
= true; // input dynamics, before pitch tracking
const cfg: ProcessorConfigcfg.ProcessorConfig.punch: PunchConfig

The input dynamics and saturation stage, which applies first.

It is disabled by default.

punch
.PunchConfig.impact: number

The transient emphasis without a unit, 0..=1.

The default is 0.5. It sets the release half-life of the adaptive follower: 5000 ms at 0, 250 ms at 0.5 and 1 ms at 1. The attack uses attack_ms.

impact
= 0.45;
const cfg: ProcessorConfigcfg.ProcessorConfig.warm: WarmConfig

The asymmetric saturation stage after Punch.

It is disabled by default.

warm
.WarmConfig.enabled: boolean

Enables the saturation stage. The default is false.

enabled
= true; // asymmetric saturation
const cfg: ProcessorConfigcfg.ProcessorConfig.warm: WarmConfig

The asymmetric saturation stage after Punch.

It is disabled by default.

warm
.WarmConfig.driveDb: number

The drive in decibels, 0..=6. The default is 3 dB. The output gain changes by -0.35 dB for each dB of drive. At 0 dB, the envelope normalization and the output high-pass filter still operate.

driveDb
= 3;
const cfg: ProcessorConfigcfg.ProcessorConfig.alien: AlienConfig

The reverse-loop effect after resynthesis.

It is disabled by default.

alien
.AlienConfig.enabled: boolean

Selects reversed playback instead of the direct input. The default is false. The recording and the phase continue to advance in both states.

enabled
= true; // reversed loop playback after resynthesis
const cfg: ProcessorConfigcfg.ProcessorConfig.alien: AlienConfig

The reverse-loop effect after resynthesis.

It is disabled by default.

alien
.AlienConfig.loopFrames: number

The loop length in frames, 1..=96000. The default is 95,999. This value is a frame count at the two rates. Thus, its duration in seconds changes with the sample rate. A short loop can be fully inside the fades.

loopFrames
= 48000;
const cfg: ProcessorConfigcfg.ProcessorConfig.reverb: ReverbConfig

The echo or reverberation stage after Alien.

It is disabled by default.

reverb
.ReverbConfig.enabled: boolean

Enables the selected network. The default is false. In bypass, the histories do not change.

enabled
= true; // the last stage before the output gain
const cfg: ProcessorConfigcfg.ProcessorConfig.reverb: ReverbConfig

The echo or reverberation stage after Alien.

It is disabled by default.

reverb
.ReverbConfig.mode: ReverbMode

The topology of the delay network. The default is [ReverbMode::Echo].

mode
= "mediumHall";
const cfg: ProcessorConfigcfg.ProcessorConfig.reverb: ReverbConfig

The echo or reverberation stage after Alien.

It is disabled by default.

reverb
.ReverbConfig.mix: number

The wet fraction, 0..=1.

The default is 0.5. The dry weight is 1 - mix. Schroeder and Binaural use a mono dry signal. The other modes keep the stereo dry input. If the network is enabled, a zero mix still advances it.

mix
= 0.22;
const cfg: ProcessorConfigcfg.ProcessorConfig.reverb: ReverbConfig

The echo or reverberation stage after Alien.

It is disabled by default.

reverb
.ReverbConfig.decay: number

The feedback control without a unit, 0..=1.

The default is 0.6. The prepared coefficient is approximately decay / 1.05. It is less than unity also at the maximum. This value is not a decay time in seconds. Some paths also use fixed feedback coefficients. Binaural does not use this control.

decay
= 0.58;
const cfg: ProcessorConfigcfg.ProcessorConfig.outputGain: number

The linear gain after all effects, 0..=64.

The default is 1 (unity). Zero mutes the output, but the internal processing continues. The gain is quantized in steps of 1/256 before the fixed-point multiplication and the final PCM clipping. Thus, a positive value below 1/256 also mutes the output.

outputGain
= 0.9;
const engine: Tuneletengine.Tunelet.setConfig(config: ProcessorConfig): void

Decodes and validates a configuration, then replaces the complete configuration with it.

State retention and drain cancellation follow the native processor. The conversion ignores additional properties of the JavaScript object.

Errors

The method throws if an object is not valid or a field is missing. After an error, all processor state and any active drain stay the same.

setConfig
(const cfg: ProcessorConfigcfg);

Timed events

Change settings at an exact frame inside a block instead of between blocks.

The optional last argument of process() is an array of { offset, event } pairs. Each event applies immediately before frame offset of the current block.

  • Offsets are zero-based and local to the call: 0 <= offset < frames.
  • Offsets must not decrease. Events with equal offsets run in array order, so a later one sees the earlier changes.
  • An empty block accepts no events. If any event is invalid, the whole block is rejected before any state changes.
Event typePayloadEffect
configureProcessorConfigReplace the whole configuration
punch, warm, resynthesis, alien, reverbThat stage’s config objectReplace every field of one stage
outputGainnumber, 0..=64Set the final gain
midiMidiEventHarmony note control
correctionMidiMidiEventCorrection keyboard
resetnoneSame as reset()
timed-events.ts
// Each event applies immediately before its frame offset within this block.
const engine: Tuneletengine.Tunelet.process(input: Int16Array, left: Int16Array, right: Int16Array, events?: BlockEvents | null): void

Processes mono input into left and right Int16Array buffers of equal length.

If the call is successful, it writes every output sample. Optional events apply before their frame, and their offsets obey the rules of TimedEvent[]. An empty block must have no events. A successful call cancels a drain, also for an empty block. The call uses the existing JavaScript and Rust PCM buffers again.

Errors

The method throws if the channel lengths are not equal. It also throws if the block is larger than maxBlockFrames or an event is not valid. The call checks the lengths and the block size before it decodes and validates the events. If an error occurs, the output buffers and the DSP state do not change.

process
(const input: Int16Array<ArrayBuffer>input, const left: Int16Array<ArrayBuffer>left, const right: Int16Array<ArrayBuffer>right, [
{ TimedEvent.offset: number

The frame offset in the block.

offset
: 0, TimedEvent.event: ControlEvent

The control change.

event
: { type: "outputGain"type: "outputGain", value: numbervalue: 0.9 } },
// A stage event replaces every field of that stage, so start from a snapshot. { TimedEvent.offset: number

The frame offset in the block.

offset
: 256, TimedEvent.event: ControlEvent

The control change.

event
: { type: "reverb"type: "reverb", value: ReverbConfigvalue: { ...const engine: Tuneletengine.Tunelet.config(): ProcessorConfig

Returns a copy of the active configuration as a new JavaScript object.

A change to the object has no effect until setConfig or a timed event applies it.

Errors

If the conversion to a JavaScript object fails, this method throws.

config
().ProcessorConfig.reverb: ReverbConfig

The echo or reverberation stage after Alien.

It is disabled by default.

reverb
, ReverbConfig.mix: number

The wet fraction, 0..=1.

The default is 0.5. The dry weight is 1 - mix. Schroeder and Binaural use a mono dry signal. The other modes keep the stereo dry input. If the network is enabled, a zero mix still advances it.

mix
: 0.4 } } },
{ TimedEvent.offset: number

The frame offset in the block.

offset
: 512, TimedEvent.event: ControlEvent

The control change.

event
: { type: "outputGain"type: "outputGain", value: numbervalue: 0.65 } },
]);

Timing tells the processor when the state changes. Smoothing, the tracking interval and the signal latency decide when you hear it.

MIDI control

Harmony voices and the correction keyboard both listen to MIDI note events delivered as timed events.

MidiEvent has three shapes: noteOn with channel, note and velocity, noteOff with channel and note, and allNotesOff. Channels are zero-based and note 69 is A4 at 440 Hz.

  • source: "midiChannels" (the default) assigns channels 0 to 3 to the four harmony voices.
  • "midiOmni" gives notes from any channel to the first free voice. "chordViaMidi" voices the held notes as a chord with registerPercent and voicingSpreadPercent.
  • Velocity sets the voice gain, roughly -24 dB at 1 and +12 dB at 127. Velocity 0 releases the note.
  • correctionMidi replaces the active correction note table until notes or enabled changes in the configuration.
midi.ts
// Harmony follows MIDI on channels 0..=3 by default (source: "midiChannels").
const engine: Tuneletengine.Tunelet.process(input: Int16Array, left: Int16Array, right: Int16Array, events?: BlockEvents | null): void

Processes mono input into left and right Int16Array buffers of equal length.

If the call is successful, it writes every output sample. Optional events apply before their frame, and their offsets obey the rules of TimedEvent[]. An empty block must have no events. A successful call cancels a drain, also for an empty block. The call uses the existing JavaScript and Rust PCM buffers again.

Errors

The method throws if the channel lengths are not equal. It also throws if the block is larger than maxBlockFrames or an event is not valid. The call checks the lengths and the block size before it decodes and validates the events. If an error occurs, the output buffers and the DSP state do not change.

process
(const input: Int16Array<ArrayBuffer>input, const left: Int16Array<ArrayBuffer>left, const right: Int16Array<ArrayBuffer>right, [
{ TimedEvent.offset: number

The frame offset in the block.

offset
: 0, TimedEvent.event: ControlEvent

The control change.

event
: { type: "midi"type: "midi", value: MidiEventvalue: { type: "noteOn"type: "noteOn", channel: numberchannel: 0, note: numbernote: 64, velocity: numbervelocity: 100 } } },
{ TimedEvent.offset: number

The frame offset in the block.

offset
: 0, TimedEvent.event: ControlEvent

The control change.

event
: { type: "midi"type: "midi", value: MidiEventvalue: { type: "noteOn"type: "noteOn", channel: numberchannel: 1, note: numbernote: 67, velocity: numbervelocity: 90 } } },
]); // Later: release one voice, or everything at once. const engine: Tuneletengine.Tunelet.process(input: Int16Array, left: Int16Array, right: Int16Array, events?: BlockEvents | null): void

Processes mono input into left and right Int16Array buffers of equal length.

If the call is successful, it writes every output sample. Optional events apply before their frame, and their offsets obey the rules of TimedEvent[]. An empty block must have no events. A successful call cancels a drain, also for an empty block. The call uses the existing JavaScript and Rust PCM buffers again.

Errors

The method throws if the channel lengths are not equal. It also throws if the block is larger than maxBlockFrames or an event is not valid. The call checks the lengths and the block size before it decodes and validates the events. If an error occurs, the output buffers and the DSP state do not change.

process
(const input: Int16Array<ArrayBuffer>input, const left: Int16Array<ArrayBuffer>left, const right: Int16Array<ArrayBuffer>right, [
{ TimedEvent.offset: number

The frame offset in the block.

offset
: 128, TimedEvent.event: ControlEvent

The control change.

event
: { type: "midi"type: "midi", value: MidiEventvalue: { type: "noteOff"type: "noteOff", channel: numberchannel: 0, note: numbernote: 64 } } },
{ TimedEvent.offset: number

The frame offset in the block.

offset
: 900, TimedEvent.event: ControlEvent

The control change.

event
: { type: "midi"type: "midi", value: MidiEventvalue: { type: "allNotesOff"type: "allNotesOff" } } },
]); // A separate keyboard can replace the active correction note table. const engine: Tuneletengine.Tunelet.process(input: Int16Array, left: Int16Array, right: Int16Array, events?: BlockEvents | null): void

Processes mono input into left and right Int16Array buffers of equal length.

If the call is successful, it writes every output sample. Optional events apply before their frame, and their offsets obey the rules of TimedEvent[]. An empty block must have no events. A successful call cancels a drain, also for an empty block. The call uses the existing JavaScript and Rust PCM buffers again.

Errors

The method throws if the channel lengths are not equal. It also throws if the block is larger than maxBlockFrames or an event is not valid. The call checks the lengths and the block size before it decodes and validates the events. If an error occurs, the output buffers and the DSP state do not change.

process
(const input: Int16Array<ArrayBuffer>input, const left: Int16Array<ArrayBuffer>left, const right: Int16Array<ArrayBuffer>right, [
{ TimedEvent.offset: number

The frame offset in the block.

offset
: 0, TimedEvent.event: ControlEvent

The control change.

event
: { type: "correctionMidi"type: "correctionMidi", value: MidiEventvalue: { type: "noteOn"type: "noteOn", channel: numberchannel: 0, note: numbernote: 62, velocity: numbervelocity: 1 } } },
]);

The engine resynthesizes the harmony voices from the tracked input. They are not oscillators, so they sound only while the input is voiced.

Feedback

Read what the tracker hears without touching the audio.

feedback() returns a snapshot. takeFeedback() returns the same snapshot and clears the stereo peak window. Neither call processes audio.

FieldMeaning
voicedTrue when the kept pitch estimate is positive and inside the reporting bounds
frequencyHz, periodSamplesInput pitch. Zero when unvoiced
targetMidiNoteThe correction target, or the input pitch as a fractional MIDI note. undefined when unvoiced
resampleRatioCorrection ratio of the last frame, 1 is unity
peakLevelLargest clipped stereo sample since the window started, 0..1
processedFramesFrames since construction or reset, as a bigint
latencyFramesNominal resynthesis latency
feedback.ts
const const fb: Feedbackfb = const engine: Tuneletengine.Tunelet.takeFeedback(): Feedback

Returns the feedback and clears only the stereo peak window.

The other feedback fields and all audio histories do not change.

Errors

If the conversion to a JavaScript object fails, this method throws. The method clears the peak only after it successfully makes a feedback object.

takeFeedback
(); // also clears the peak window
if (const fb: Feedbackfb.Feedback.voiced: boolean

This field is true if the current pitch estimate is positive and inside the current reporting bounds.

This is not an amplitude gate or a confidence score. A tracking result can stay between detector updates.

voiced
) {
var console: Consoleconsole.Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(const fb: Feedbackfb.Feedback.frequencyHz: number

The reported input pitch frequency in hertz.

The value comes from the sample rate and the current tracked period. It is zero if the estimate is not reported.

frequencyHz
.Number.toFixed(fractionDigits?: number): string

Returns a string representing a number in fixed-point notation.

@paramfractionDigits Number of digits after the decimal point. Must be in the range 0 - 20, inclusive.
toFixed
(1), "Hz", "target", const fb: Feedbackfb.Feedback.targetMidiNote: number | undefined

The correction target or the input pitch, as a MIDI note number.

The value is the selected correction pitch class if all of these conditions are true:

  • Correction is enabled.
  • The active note table is not empty.
  • A tracking update selected a target after the last table change, reset or tracking restart.

The pitch class is in the octave nearest to the reported input. In other cases, the value is the input pitch as a fractional MIDI note. It is not the final pitch of shifted output or harmony output.

The value is None if voiced is false, and only then. This occurs if the input is unvoiced or if its frequency is outside the current reporting bounds. A target table that has no selected target does not give None.

targetMidiNote
);
} var console: Consoleconsole.Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
("peak", const fb: Feedbackfb.Feedback.peakLevel: number

The largest absolute clipped stereo sample, divided by 32768.

The peak window starts at construction, at a reset, at a change of stereo output or at a call to [Processor::take_feedback]. The value is in 0..=32767/32768. The mono downmix occurs after this measurement. [Processor::feedback] does not clear the value.

peakLevel
, "ratio", const fb: Feedbackfb.Feedback.resampleRatio: number

The pitch-correction ratio from the last processed frame, where 1 is unity.

It does not include the configured pitch shift or the ratios of individual voices. It is zero before processing, after a reset or after a change of stereo output.

resampleRatio
);
var console: Consoleconsole.Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
("frames", const fb: Feedbackfb.Feedback.processedFrames: bigint

The number of frames processed since construction or the last reset, with drain frames included.

The count saturates at u64::MAX. Drain completion resets it in the last drain call.

processedFrames
, "latency", const fb: Feedbackfb.Feedback.latencyFrames: number

The nominal resynthesis latency, as [Processor::latency_frames] gives.

latencyFrames
); // bigint, number

The reporting bounds are resynthesis.pitch.minFrequencyHz and maxFrequencyHz, 40 to 1100 Hz by default. They affect only what is reported, not the tracking itself.

Platforms

Browser

Run the engine on the main thread, on the audio thread or in a Web Worker.

Main thread

On the main thread, use the package as in Node.js. loadTunelet() fetches tunelet_bg.wasm from the package folder. A long render blocks the page, so use a Web Worker for long files.

main.ts
import { function loadTunelet(source?: InitInput | Promise<InitInput>): Promise<InitOutput>

Initializes the Wasm module one time for this imported package instance.

If you give no argument, the function finds the packaged binary relative to this module. Node.js reads a file: URL, and browsers fetch the served asset. An import of the package does not load Wasm. In browsers, serve the assets over HTTP(S), or give the bytes or a compiled module as the source.

For a custom deployment, you can give one of these sources:

  • A URL.
  • A fetch Response.
  • A byte buffer (BufferSource).
  • A compiled WebAssembly.Module.
  • A promise for one of these.

A string source obeys fetch rules. For a Node.js file: source, use a URL object. A compiled module must use imports that agree with the packaged bindings.

Concurrent calls share one promise, and the first call selects the source. After a successful initialization, later calls use it again and do not load Wasm again, whatever their arguments are. If an attempt fails, the promise rejects and the function clears the cached promise. Then you can try again. File, network and Wasm errors go to the caller. The function does not try a different load method after an error.

@paramsource The optional Wasm source. The default is the binary in this package.@returnsThe initialized Wasm exports. Use the re-exported Tunelet class for processors.
loadTunelet
, class Tunelet

A stateful PCM processor that JavaScript uses as the Tunelet class.

The input is signed 16-bit mono PCM. The output is planar stereo or an in-place mono downmix. The quantization, latency and configuration behavior are the same as in the native processor. Buffer lengths count frames, not bytes.

These rules apply to the buffers:

  • Separate stereo output arrays must not overlap.
  • The input can overlap an output, because the class copies the input first.
  • All buffers must stay attached until each call is complete.

If a call that processes audio fails, it throws a JavaScript Error before the audio state or the output changes. The class rejects malformed configuration objects and malformed event objects. It also rejects values that are out of range. The class decodes objects inside the methods that can fail. Thus, if a conversion fails, Rust releases the temporary values as usual.

Tunelet
} from "tunelet";
await function loadTunelet(source?: InitInput | Promise<InitInput>): Promise<InitOutput>

Initializes the Wasm module one time for this imported package instance.

If you give no argument, the function finds the packaged binary relative to this module. Node.js reads a file: URL, and browsers fetch the served asset. An import of the package does not load Wasm. In browsers, serve the assets over HTTP(S), or give the bytes or a compiled module as the source.

For a custom deployment, you can give one of these sources:

  • A URL.
  • A fetch Response.
  • A byte buffer (BufferSource).
  • A compiled WebAssembly.Module.
  • A promise for one of these.

A string source obeys fetch rules. For a Node.js file: source, use a URL object. A compiled module must use imports that agree with the packaged bindings.

Concurrent calls share one promise, and the first call selects the source. After a successful initialization, later calls use it again and do not load Wasm again, whatever their arguments are. If an attempt fails, the promise rejects and the function clears the cached promise. Then you can try again. File, network and Wasm errors go to the caller. The function does not try a different load method after an error.

@paramsource The optional Wasm source. The default is the binary in this package.@returnsThe initialized Wasm exports. Use the re-exported Tunelet class for processors.
loadTunelet
();
const const engine: Tuneletengine = new new Tunelet(sample_rate: number, max_block_frames: number, config?: ProcessorConfig | null): Tunelet

Prepares a processor and its reusable PCM buffers.

sample_rate must be 44100 or 48000. max_block_frames must be an integer in 1..=4294967295. It is a limit for each call, and smaller blocks and empty blocks are also valid. If you omit config, the processor uses the default configuration. The constructor validates the settings before it allocates the processor or the PCM buffers. In TypeScript, use new Tunelet(sampleRate, maxBlockFrames, config).

Errors

The constructor throws if one of these conditions occurs:

  • The rate is not 44100 or 48000.
  • The block limit is not an integer or is out of range.
  • The configuration object is malformed or a field is missing.
  • The configuration is not valid.
  • The reservation of a PCM buffer fails.

The available Wasm memory must also have sufficient space for the DSP histories.

Tunelet
(48000, 4096);
const const input: Int16Array<ArrayBuffer>input = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(4096); // Mono 16-bit PCM
const const left: Int16Array<ArrayBuffer>left = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(4096);
const const right: Int16Array<ArrayBuffer>right = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(4096);
const engine: Tuneletengine.Tunelet.process(input: Int16Array, left: Int16Array, right: Int16Array, events?: BlockEvents | null): void

Processes mono input into left and right Int16Array buffers of equal length.

If the call is successful, it writes every output sample. Optional events apply before their frame, and their offsets obey the rules of TimedEvent[]. An empty block must have no events. A successful call cancels a drain, also for an empty block. The call uses the existing JavaScript and Rust PCM buffers again.

Errors

The method throws if the channel lengths are not equal. It also throws if the block is larger than maxBlockFrames or an event is not valid. The call checks the lengths and the block size before it decodes and validates the events. If an error occurs, the output buffers and the DSP state do not change.

process
(const input: Int16Array<ArrayBuffer>input, const left: Int16Array<ArrayBuffer>left, const right: Int16Array<ArrayBuffer>right);

If a bundler builds the page, it must keep the Wasm file of the package. Without a bundler, serve dist/ and pkg/ of the package, and map tunelet to dist/index.js with an import map.

AudioWorklet

For live input, run the engine on the audio thread, away from layout and garbage collection.

  1. Create the AudioContext at 44,100 or 48,000 Hz and register your worklet module.
  2. Serve tunelet_bg.wasm as a static asset and compile it on the main thread with WebAssembly.compileStreaming. The worklet scope has no fetch().
  3. In the worklet, install a TextDecoder and TextEncoder polyfill before you import the bindings. The worklet scope has neither, and the bindings need both as soon as they load.
  4. Pass the compiled module through processorOptions, call initSync({ module }) in the processor constructor, then create the Tunelet.
  5. Convert each 128-frame Float32 block to 16-bit PCM, process it, and convert back.
main.ts
const const context: AudioContextcontext = new var AudioContext: new (contextOptions?: AudioContextOptions) => AudioContext

The AudioContext interface represents an audio-processing graph built from audio modules linked together, each represented by an AudioNode.

MDN Reference

AudioContext
({ AudioContextOptions.sampleRate?: number | undefinedsampleRate: 48000 });
await const context: AudioContextcontext.BaseAudioContext.audioWorklet: AudioWorklet

The audioWorklet read-only property of the processing. Available only in secure contexts.

MDN Reference

audioWorklet
.Worklet.addModule(moduleURL: string | URL, options?: WorkletOptions): Promise<void>

The addModule() method of the adds it to the current Worklet.

MDN Reference

addModule
(new var URL: new (url: string | URL, base?: string | URL) => URL

The URL interface is used to parse, construct, normalize, and encode URL.

MDN Reference

URL
("./vocal-processor.js", import.meta.ImportMeta.url: stringurl));
// Serve tunelet_bg.wasm from the package's pkg/ folder as a static asset. // Compile it here: the worklet scope has no fetch(). const const module: WebAssembly.Modulemodule = await WebAssembly.function WebAssembly.compileStreaming(source: Response | PromiseLike<Response>): Promise<WebAssembly.Module>compileStreaming(function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>fetch("/engine/tunelet_bg.wasm")); const const node: AudioWorkletNodenode = new var AudioWorkletNode: new (context: BaseAudioContext, name: string, options?: AudioWorkletNodeOptions) => AudioWorkletNode

The AudioWorkletNode interface of the Web Audio API represents a base class for a user-defined AudioNode, which can be connected to an audio routing graph along with other nodes. Available only in secure contexts.

MDN Reference

AudioWorkletNode
(const context: AudioContextcontext, "vocal-processor", {
AudioWorkletNodeOptions.numberOfInputs?: number | undefinednumberOfInputs: 1, AudioWorkletNodeOptions.numberOfOutputs?: number | undefinednumberOfOutputs: 1, AudioWorkletNodeOptions.outputChannelCount?: number[] | undefinedoutputChannelCount: [2], AudioWorkletNodeOptions.processorOptions?: anyprocessorOptions: { module: WebAssembly.Modulemodule }, }); const const mic: MediaStreammic = await var navigator: Navigator

The Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.

MDN Reference

navigator
.Navigator.mediaDevices: MediaDevices

The mediaDevices read-only property of the Navigator interface returns a MediaDevices object, which provides access to connected media input devices like cameras and microphones, as well as screen sharing. Available only in secure contexts.

MDN Reference

mediaDevices
.MediaDevices.getUserMedia(constraints?: MediaStreamConstraints): Promise<MediaStream>

The getUserMedia() method of the MediaDevices interface prompts the user for permission to use a media input which produces a MediaStream with tracks containing the requested types of media.

MDN Reference

getUserMedia
({ MediaStreamConstraints.audio?: boolean | MediaTrackConstraints | undefinedaudio: true });
const context: AudioContextcontext.AudioContext.createMediaStreamSource(mediaStream: MediaStream): MediaStreamAudioSourceNode

The createMediaStreamSource() method of the AudioContext Interface is used to create a new MediaStreamAudioSourceNode object, given a media stream (say, from a MediaDevices.getUserMedia instance), the audio from which can then be played and manipulated.

MDN Reference

createMediaStreamSource
(const mic: MediaStreammic).AudioNode.connect(destinationNode: AudioNode, output?: number, input?: number): AudioNode (+1 overload)

The connect() method of the AudioNode interface lets you connect one of the node's outputs to a target, which may be either another AudioNode (thereby directing the sound data to the specified node) or an change the value of that parameter over time.

MDN Reference

connect
(const node: AudioWorkletNodenode).AudioNode.connect(destinationNode: AudioNode, output?: number, input?: number): AudioNode (+1 overload)

The connect() method of the AudioNode interface lets you connect one of the node's outputs to a target, which may be either another AudioNode (thereby directing the sound data to the specified node) or an change the value of that parameter over time.

MDN Reference

connect
(const context: AudioContextcontext.BaseAudioContext.destination: AudioDestinationNode

The destination property of the BaseAudioContext interface returns an AudioDestinationNode representing the final destination of all audio in the context.

MDN Reference

destination
);
text-encoding.ts
// AudioWorkletGlobalScope has no TextDecoder or TextEncoder; the Wasm bindings need both.
const const scope: anyscope = module globalThisglobalThis as any;

const scope: anyscope.TextDecoder ??= class {
  TextDecoder.decode(bytes?: Uint8Array): stringdecode(bytes: Uint8Array<ArrayBufferLike> | undefinedbytes?: interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
): string {
if (!bytes: Uint8Array<ArrayBufferLike> | undefinedbytes) return ""; let let out: stringout = ""; for (let let i: numberi = 0; let i: numberi < bytes: Uint8Array<ArrayBufferLike>bytes.Uint8Array<ArrayBufferLike>.length: number

The length of the array.

length
; ) {
let let c: numberc = bytes: Uint8Array<ArrayBufferLike>bytes[let i: numberi++]; if (let c: numberc >= 0xf0) let c: numberc = ((let c: numberc & 7) << 18) | ((bytes: Uint8Array<ArrayBufferLike>bytes[let i: numberi++] & 63) << 12) | ((bytes: Uint8Array<ArrayBufferLike>bytes[let i: numberi++] & 63) << 6) | (bytes: Uint8Array<ArrayBufferLike>bytes[let i: numberi++] & 63); else if (let c: numberc >= 0xe0) let c: numberc = ((let c: numberc & 15) << 12) | ((bytes: Uint8Array<ArrayBufferLike>bytes[let i: numberi++] & 63) << 6) | (bytes: Uint8Array<ArrayBufferLike>bytes[let i: numberi++] & 63); else if (let c: numberc >= 0xc0) let c: numberc = ((let c: numberc & 31) << 6) | (bytes: Uint8Array<ArrayBufferLike>bytes[let i: numberi++] & 63); let out: stringout += var String: StringConstructor

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
.StringConstructor.fromCodePoint(...codePoints: number[]): string

Return the String value whose elements are, in order, the elements in the List elements. If length is 0, the empty string is returned.

fromCodePoint
(let c: numberc);
} return let out: stringout; } }; const scope: anyscope.TextEncoder ??= class { TextEncoder.encode(text: string): Uint8Arrayencode(text: stringtext: string): interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
{
const const out: number[]out: number[] = []; for (const const ch: stringch of text: stringtext) { const const c: numberc = const ch: stringch.String.codePointAt(pos: number): number | undefined

Returns a nonnegative integer Number less than 1114112 (0x110000) that is the code point value of the UTF-16 encoded code point starting at the string element at position pos in the String resulting from converting this object to a String. If there is no element at that position, the result is undefined. If a valid UTF-16 surrogate pair does not begin at pos, the result is the code unit at pos.

codePointAt
(0)!;
if (const c: numberc < 0x80) const out: number[]out.Array<number>.push(...items: number[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@paramitems New elements to add to the array.
push
(const c: numberc);
else if (const c: numberc < 0x800) const out: number[]out.Array<number>.push(...items: number[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@paramitems New elements to add to the array.
push
(0xc0 | (const c: numberc >> 6), 0x80 | (const c: numberc & 63));
else if (const c: numberc < 0x10000) const out: number[]out.Array<number>.push(...items: number[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@paramitems New elements to add to the array.
push
(0xe0 | (const c: numberc >> 12), 0x80 | ((const c: numberc >> 6) & 63), 0x80 | (const c: numberc & 63));
else const out: number[]out.Array<number>.push(...items: number[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@paramitems New elements to add to the array.
push
(0xf0 | (const c: numberc >> 18), 0x80 | ((const c: numberc >> 12) & 63), 0x80 | ((const c: numberc >> 6) & 63), 0x80 | (const c: numberc & 63));
} return var Uint8Array: Uint8ArrayConstructor

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
.Uint8ArrayConstructor.from(elements: Iterable<number>): Uint8Array<ArrayBuffer> (+3 overloads)

Creates an array from an array-like or iterable object.

@paramelements An iterable object to convert to an array.
from
(const out: number[]out);
}
TextEncoder.encodeInto(text: string, view: Uint8Array): {
    read: number;
    written: number;
}
encodeInto
(text: stringtext: string, view: Uint8Array<ArrayBufferLike>view: interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
) {
const const bytes: Uint8Array<ArrayBufferLike>bytes = this.TextEncoder.encode(text: string): Uint8Arrayencode(text: stringtext); view: Uint8Array<ArrayBufferLike>view.Uint8Array<ArrayBufferLike>.set(array: ArrayLike<number>, offset?: number): void

Sets a value or an array of values.

@paramarray A typed or untyped array of values to set.@paramoffset The index in the current array at which the values are to be written.
set
(const bytes: Uint8Array<ArrayBufferLike>bytes);
return { read: numberread: text: stringtext.String.length: number

Returns the length of a String object.

length
, written: numberwritten: const bytes: Uint8Array<ArrayBufferLike>bytes.Uint8Array<ArrayBufferLike>.length: number

The length of the array.

length
};
} }; export {};
vocal-processor.ts
import "./text-encoding"; // must come first
import { 
function initSync(module: {
    module: SyncInitInput;
} | SyncInitInput): InitOutput

Instantiates the given module, which can either be bytes or a precompiled WebAssembly.Module.

@parammodule - Passing SyncInitInput directly is deprecated.@returns
initSync
, class Tunelet

A stateful PCM processor that JavaScript uses as the Tunelet class.

The input is signed 16-bit mono PCM. The output is planar stereo or an in-place mono downmix. The quantization, latency and configuration behavior are the same as in the native processor. Buffer lengths count frames, not bytes.

These rules apply to the buffers:

  • Separate stereo output arrays must not overlap.
  • The input can overlap an output, because the class copies the input first.
  • All buffers must stay attached until each call is complete.

If a call that processes audio fails, it throws a JavaScript Error before the audio state or the output changes. The class rejects malformed configuration objects and malformed event objects. It also rejects values that are out of range. The class decodes objects inside the methods that can fail. Thus, if a conversion fails, Rust releases the temporary values as usual.

Tunelet
} from "tunelet/wasm";
class class VocalProcessorVocalProcessor extends class AudioWorkletProcessorAudioWorkletProcessor { private readonly VocalProcessor.engine: Tuneletengine: class Tunelet

A stateful PCM processor that JavaScript uses as the Tunelet class.

The input is signed 16-bit mono PCM. The output is planar stereo or an in-place mono downmix. The quantization, latency and configuration behavior are the same as in the native processor. Buffer lengths count frames, not bytes.

These rules apply to the buffers:

  • Separate stereo output arrays must not overlap.
  • The input can overlap an output, because the class copies the input first.
  • All buffers must stay attached until each call is complete.

If a call that processes audio fails, it throws a JavaScript Error before the audio state or the output changes. The class rejects malformed configuration objects and malformed event objects. It also rejects values that are out of range. The class decodes objects inside the methods that can fail. Thus, if a conversion fails, Rust releases the temporary values as usual.

Tunelet
;
private readonly VocalProcessor.pcmIn: Int16Array<ArrayBuffer>pcmIn = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(128);
private readonly VocalProcessor.pcmL: Int16Array<ArrayBuffer>pcmL = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(128);
private readonly VocalProcessor.pcmR: Int16Array<ArrayBuffer>pcmR = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(128);
constructor(
options: {
    processorOptions: {
        module: WebAssembly.Module;
    };
}
options
: {
processorOptions: {
    module: WebAssembly.Module;
}
processorOptions
: { module: WebAssembly.Modulemodule: WebAssembly.interface WebAssembly.ModuleModule } }) {
super(); // Synchronous initialisation from the precompiled module.
function initSync(module: {
    module: SyncInitInput;
} | SyncInitInput): InitOutput

Instantiates the given module, which can either be bytes or a precompiled WebAssembly.Module.

@parammodule - Passing SyncInitInput directly is deprecated.@returns
initSync
({ module: SyncInitInputmodule:
options: {
    processorOptions: {
        module: WebAssembly.Module;
    };
}
options
.
processorOptions: {
    module: WebAssembly.Module;
}
processorOptions
.module: WebAssembly.Modulemodule });
this.VocalProcessor.engine: Tuneletengine = new new Tunelet(sample_rate: number, max_block_frames: number, config?: ProcessorConfig | null): Tunelet

Prepares a processor and its reusable PCM buffers.

sample_rate must be 44100 or 48000. max_block_frames must be an integer in 1..=4294967295. It is a limit for each call, and smaller blocks and empty blocks are also valid. If you omit config, the processor uses the default configuration. The constructor validates the settings before it allocates the processor or the PCM buffers. In TypeScript, use new Tunelet(sampleRate, maxBlockFrames, config).

Errors

The constructor throws if one of these conditions occurs:

  • The rate is not 44100 or 48000.
  • The block limit is not an integer or is out of range.
  • The configuration object is malformed or a field is missing.
  • The configuration is not valid.
  • The reservation of a PCM buffer fails.

The available Wasm memory must also have sufficient space for the DSP histories.

Tunelet
(const sampleRate: numbersampleRate, 128);
} VocalProcessor.process(inputs: Float32Array[][], outputs: Float32Array[][]): booleanprocess(inputs: Float32Array<ArrayBufferLike>[][]inputs: interface Float32Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 32-bit float values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Float32Array
[][], outputs: Float32Array<ArrayBufferLike>[][]outputs: interface Float32Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 32-bit float values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Float32Array
[][]): boolean {
const const input: Float32Array<ArrayBufferLike>input = inputs: Float32Array<ArrayBufferLike>[][]inputs[0]?.[0]; const const outL: Float32Array<ArrayBufferLike>outL = outputs: Float32Array<ArrayBufferLike>[][]outputs[0]?.[0]; const const outR: Float32Array<ArrayBufferLike>outR = outputs: Float32Array<ArrayBufferLike>[][]outputs[0]?.[1] ?? const outL: Float32Array<ArrayBufferLike>outL; if (!const input: Float32Array<ArrayBufferLike>input || !const outL: Float32Array<ArrayBufferLike>outL || !const outR: Float32Array<ArrayBufferLike>outR) return true; const const frames: numberframes = const input: Float32Array<ArrayBufferLike>input.Float32Array<ArrayBufferLike>.length: number

The length of the array.

length
;
for (let let i: numberi = 0; let i: numberi < const frames: numberframes; let i: numberi++) { const const s: numbers = var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.Math.max(...values: number[]): number

Returns the larger of a set of supplied numeric expressions.

@paramvalues Numeric expressions to be evaluated.
max
(-1, var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.Math.min(...values: number[]): number

Returns the smaller of a set of supplied numeric expressions.

@paramvalues Numeric expressions to be evaluated.
min
(1, const input: Float32Array<ArrayBufferLike>input[let i: numberi]));
this.VocalProcessor.pcmIn: Int16Array<ArrayBuffer>pcmIn[let i: numberi] = const s: numbers < 0 ? const s: numbers * 32768 : const s: numbers * 32767; } this.VocalProcessor.engine: Tuneletengine.Tunelet.process(input: Int16Array, left: Int16Array, right: Int16Array, events?: BlockEvents | null): void

Processes mono input into left and right Int16Array buffers of equal length.

If the call is successful, it writes every output sample. Optional events apply before their frame, and their offsets obey the rules of TimedEvent[]. An empty block must have no events. A successful call cancels a drain, also for an empty block. The call uses the existing JavaScript and Rust PCM buffers again.

Errors

The method throws if the channel lengths are not equal. It also throws if the block is larger than maxBlockFrames or an event is not valid. The call checks the lengths and the block size before it decodes and validates the events. If an error occurs, the output buffers and the DSP state do not change.

process
(
this.VocalProcessor.pcmIn: Int16Array<ArrayBuffer>pcmIn.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, const frames: numberframes),
this.VocalProcessor.pcmL: Int16Array<ArrayBuffer>pcmL.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, const frames: numberframes),
this.VocalProcessor.pcmR: Int16Array<ArrayBuffer>pcmR.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, const frames: numberframes),
); for (let let i: numberi = 0; let i: numberi < const frames: numberframes; let i: numberi++) { const outL: Float32Array<ArrayBufferLike>outL[let i: numberi] = this.VocalProcessor.pcmL: Int16Array<ArrayBuffer>pcmL[let i: numberi] / 32768; const outR: Float32Array<ArrayBufferLike>outR[let i: numberi] = this.VocalProcessor.pcmR: Int16Array<ArrayBuffer>pcmR[let i: numberi] / 32768; } return true; } } function registerProcessor(name: string, processor: unknown): voidregisterProcessor("vocal-processor", class VocalProcessorVocalProcessor);

AudioWorklet requires a secure context (HTTPS or localhost). The worklet module imports tunelet/wasm and the polyfill, so bundle it into one file, or serve the package’s pkg/tunelet.js and import it by URL.

Web Worker

A Web Worker also has all that loadTunelet() needs. Send each block of up to 4,096 frames to the worker. The page stays responsive while the worker processes it.

tunelet-worker.ts
import { function loadTunelet(source?: InitInput | Promise<InitInput>): Promise<InitOutput>

Initializes the Wasm module one time for this imported package instance.

If you give no argument, the function finds the packaged binary relative to this module. Node.js reads a file: URL, and browsers fetch the served asset. An import of the package does not load Wasm. In browsers, serve the assets over HTTP(S), or give the bytes or a compiled module as the source.

For a custom deployment, you can give one of these sources:

  • A URL.
  • A fetch Response.
  • A byte buffer (BufferSource).
  • A compiled WebAssembly.Module.
  • A promise for one of these.

A string source obeys fetch rules. For a Node.js file: source, use a URL object. A compiled module must use imports that agree with the packaged bindings.

Concurrent calls share one promise, and the first call selects the source. After a successful initialization, later calls use it again and do not load Wasm again, whatever their arguments are. If an attempt fails, the promise rejects and the function clears the cached promise. Then you can try again. File, network and Wasm errors go to the caller. The function does not try a different load method after an error.

@paramsource The optional Wasm source. The default is the binary in this package.@returnsThe initialized Wasm exports. Use the re-exported Tunelet class for processors.
loadTunelet
, class Tunelet

A stateful PCM processor that JavaScript uses as the Tunelet class.

The input is signed 16-bit mono PCM. The output is planar stereo or an in-place mono downmix. The quantization, latency and configuration behavior are the same as in the native processor. Buffer lengths count frames, not bytes.

These rules apply to the buffers:

  • Separate stereo output arrays must not overlap.
  • The input can overlap an output, because the class copies the input first.
  • All buffers must stay attached until each call is complete.

If a call that processes audio fails, it throws a JavaScript Error before the audio state or the output changes. The class rejects malformed configuration objects and malformed event objects. It also rejects values that are out of range. The class decodes objects inside the methods that can fail. Thus, if a conversion fails, Rust releases the temporary values as usual.

Tunelet
} from "tunelet";
const const ready: Promise<Tunelet>ready = function loadTunelet(source?: InitInput | Promise<InitInput>): Promise<InitOutput>

Initializes the Wasm module one time for this imported package instance.

If you give no argument, the function finds the packaged binary relative to this module. Node.js reads a file: URL, and browsers fetch the served asset. An import of the package does not load Wasm. In browsers, serve the assets over HTTP(S), or give the bytes or a compiled module as the source.

For a custom deployment, you can give one of these sources:

  • A URL.
  • A fetch Response.
  • A byte buffer (BufferSource).
  • A compiled WebAssembly.Module.
  • A promise for one of these.

A string source obeys fetch rules. For a Node.js file: source, use a URL object. A compiled module must use imports that agree with the packaged bindings.

Concurrent calls share one promise, and the first call selects the source. After a successful initialization, later calls use it again and do not load Wasm again, whatever their arguments are. If an attempt fails, the promise rejects and the function clears the cached promise. Then you can try again. File, network and Wasm errors go to the caller. The function does not try a different load method after an error.

@paramsource The optional Wasm source. The default is the binary in this package.@returnsThe initialized Wasm exports. Use the re-exported Tunelet class for processors.
loadTunelet
().Promise<InitOutput>.then<Tunelet, never>(onfulfilled?: ((value: InitOutput) => Tunelet | PromiseLike<Tunelet>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<Tunelet>

Attaches callbacks for the resolution and/or rejection of the Promise.

@paramonfulfilled The callback to execute when the Promise is resolved.@paramonrejected The callback to execute when the Promise is rejected.@returnsA Promise for the completion of which ever callback is executed.
then
(() => new new Tunelet(sample_rate: number, max_block_frames: number, config?: ProcessorConfig | null): Tunelet

Prepares a processor and its reusable PCM buffers.

sample_rate must be 44100 or 48000. max_block_frames must be an integer in 1..=4294967295. It is a limit for each call, and smaller blocks and empty blocks are also valid. If you omit config, the processor uses the default configuration. The constructor validates the settings before it allocates the processor or the PCM buffers. In TypeScript, use new Tunelet(sampleRate, maxBlockFrames, config).

Errors

The constructor throws if one of these conditions occurs:

  • The rate is not 44100 or 48000.
  • The block limit is not an integer or is out of range.
  • The configuration object is malformed or a field is missing.
  • The configuration is not valid.
  • The reservation of a PCM buffer fails.

The available Wasm memory must also have sufficient space for the DSP histories.

Tunelet
(48000, 4096));
var self: Window & typeof globalThis

The Window.self read-only property returns the window itself, as a WindowProxy.

MDN Reference

self
.onmessage: (((this: WindowEventHandlers, ev: MessageEvent) => any) & ((this: Window, ev: MessageEvent) => any)) | nullonmessage = async (event: MessageEvent<Int16Array<ArrayBufferLike>>event: interface MessageEvent<T = any>

The MessageEvent interface represents a message received by a target object.

MDN Reference

MessageEvent
<interface Int16Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 16-bit signed integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Int16Array
>) => {
const const engine: Tuneletengine = await const ready: Promise<Tunelet>ready; const const input: Int16Array<ArrayBufferLike>input = event: MessageEvent<Int16Array<ArrayBufferLike>>event.MessageEvent<Int16Array<ArrayBufferLike>>.data: Int16Array<ArrayBufferLike>

The data read-only property of the The data sent by the message emitter; this can be any data type, depending on what originated this event.

MDN Reference

data
;
const const left: Int16Array<ArrayBuffer>left = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(const input: Int16Array<ArrayBufferLike>input.Int16Array<ArrayBufferLike>.length: number

The length of the array.

length
);
const const right: Int16Array<ArrayBuffer>right = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(const input: Int16Array<ArrayBufferLike>input.Int16Array<ArrayBufferLike>.length: number

The length of the array.

length
);
const engine: Tuneletengine.Tunelet.process(input: Int16Array, left: Int16Array, right: Int16Array, events?: BlockEvents | null): void

Processes mono input into left and right Int16Array buffers of equal length.

If the call is successful, it writes every output sample. Optional events apply before their frame, and their offsets obey the rules of TimedEvent[]. An empty block must have no events. A successful call cancels a drain, also for an empty block. The call uses the existing JavaScript and Rust PCM buffers again.

Errors

The method throws if the channel lengths are not equal. It also throws if the block is larger than maxBlockFrames or an event is not valid. The call checks the lengths and the block size before it decodes and validates the events. If an error occurs, the output buffers and the DSP state do not change.

process
(const input: Int16Array<ArrayBufferLike>input, const left: Int16Array<ArrayBuffer>left, const right: Int16Array<ArrayBuffer>right);
var self: Window & typeof globalThis

The Window.self read-only property returns the window itself, as a WindowProxy.

MDN Reference

self
.function postMessage(message: any, options?: WindowPostMessageOptions): void (+1 overload)

The window.postMessage() method safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.

MDN Reference

postMessage
({ left: Int16Array<ArrayBuffer>left, right: Int16Array<ArrayBuffer>right }, { StructuredSerializeOptions.transfer?: Transferable[] | undefinedtransfer: [const left: Int16Array<ArrayBuffer>left.Int16Array<ArrayBuffer>.buffer: ArrayBuffer

The ArrayBuffer instance referenced by the array.

buffer
, const right: Int16Array<ArrayBuffer>right.Int16Array<ArrayBuffer>.buffer: ArrayBuffer

The ArrayBuffer instance referenced by the array.

buffer
] });
};
main.ts
const const worker: Workerworker = new var Worker: new (scriptURL: string | URL, options?: WorkerOptions) => Worker

The Worker interface of the Web Workers API represents a background task that can be created via script, which can send messages back to its creator.

MDN Reference

Worker
(new var URL: new (url: string | URL, base?: string | URL) => URL

The URL interface is used to parse, construct, normalize, and encode URL.

MDN Reference

URL
("./tunelet-worker.js", import.meta.ImportMeta.url: stringurl), { WorkerOptions.type?: WorkerType | undefinedtype: "module" });
const worker: Workerworker.MessageEventTarget<Worker>.onmessage: ((this: Worker, ev: MessageEvent) => any) | nullonmessage = (
event: MessageEvent<{
    left: Int16Array;
    right: Int16Array;
}>
event
: interface MessageEvent<T = any>

The MessageEvent interface represents a message received by a target object.

MDN Reference

MessageEvent
<{ left: Int16Array<ArrayBufferLike>left: interface Int16Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 16-bit signed integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Int16Array
; right: Int16Array<ArrayBufferLike>right: interface Int16Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 16-bit signed integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Int16Array
}>) => {
const { const left: Int16Array<ArrayBufferLike>left, const right: Int16Array<ArrayBufferLike>right } =
event: MessageEvent<{
    left: Int16Array;
    right: Int16Array;
}>
event
.
MessageEvent<{ left: Int16Array; right: Int16Array; }>.data: {
    left: Int16Array;
    right: Int16Array;
}

The data read-only property of the The data sent by the message emitter; this can be any data type, depending on what originated this event.

MDN Reference

data
; // Use the stereo block.
}; const worker: Workerworker.Worker.postMessage(message: any, transfer: Transferable[]): void (+1 overload)

The postMessage() method of the Worker interface sends a message to the worker.

MDN Reference

postMessage
(const block: Int16Array<ArrayBufferLike>block, [const block: Int16Array<ArrayBufferLike>block.Int16Array<ArrayBufferLike>.buffer: ArrayBufferLike

The ArrayBuffer instance referenced by the array.

buffer
]);

The import map of the page does not apply in a worker. Without a bundler, import dist/index.js by its URL.

Node.js

The same ES module renders files and streams offline. No bundler required.

In Node.js, loadTunelet() reads the packaged binary from a file: URL relative to the module. Pass your own URL, Response, byte buffer or compiled WebAssembly.Module to load it from elsewhere. The first call decides, and later arguments are ignored.

render.ts
import { function loadTunelet(source?: InitInput | Promise<InitInput>): Promise<InitOutput>

Initializes the Wasm module one time for this imported package instance.

If you give no argument, the function finds the packaged binary relative to this module. Node.js reads a file: URL, and browsers fetch the served asset. An import of the package does not load Wasm. In browsers, serve the assets over HTTP(S), or give the bytes or a compiled module as the source.

For a custom deployment, you can give one of these sources:

  • A URL.
  • A fetch Response.
  • A byte buffer (BufferSource).
  • A compiled WebAssembly.Module.
  • A promise for one of these.

A string source obeys fetch rules. For a Node.js file: source, use a URL object. A compiled module must use imports that agree with the packaged bindings.

Concurrent calls share one promise, and the first call selects the source. After a successful initialization, later calls use it again and do not load Wasm again, whatever their arguments are. If an attempt fails, the promise rejects and the function clears the cached promise. Then you can try again. File, network and Wasm errors go to the caller. The function does not try a different load method after an error.

@paramsource The optional Wasm source. The default is the binary in this package.@returnsThe initialized Wasm exports. Use the re-exported Tunelet class for processors.
loadTunelet
, class Tunelet

A stateful PCM processor that JavaScript uses as the Tunelet class.

The input is signed 16-bit mono PCM. The output is planar stereo or an in-place mono downmix. The quantization, latency and configuration behavior are the same as in the native processor. Buffer lengths count frames, not bytes.

These rules apply to the buffers:

  • Separate stereo output arrays must not overlap.
  • The input can overlap an output, because the class copies the input first.
  • All buffers must stay attached until each call is complete.

If a call that processes audio fails, it throws a JavaScript Error before the audio state or the output changes. The class rejects malformed configuration objects and malformed event objects. It also rejects values that are out of range. The class decodes objects inside the methods that can fail. Thus, if a conversion fails, Rust releases the temporary values as usual.

Tunelet
} from "tunelet";
// Node.js reads the packaged binary from disk. No fetch, no bundler. await function loadTunelet(source?: InitInput | Promise<InitInput>): Promise<InitOutput>

Initializes the Wasm module one time for this imported package instance.

If you give no argument, the function finds the packaged binary relative to this module. Node.js reads a file: URL, and browsers fetch the served asset. An import of the package does not load Wasm. In browsers, serve the assets over HTTP(S), or give the bytes or a compiled module as the source.

For a custom deployment, you can give one of these sources:

  • A URL.
  • A fetch Response.
  • A byte buffer (BufferSource).
  • A compiled WebAssembly.Module.
  • A promise for one of these.

A string source obeys fetch rules. For a Node.js file: source, use a URL object. A compiled module must use imports that agree with the packaged bindings.

Concurrent calls share one promise, and the first call selects the source. After a successful initialization, later calls use it again and do not load Wasm again, whatever their arguments are. If an attempt fails, the promise rejects and the function clears the cached promise. Then you can try again. File, network and Wasm errors go to the caller. The function does not try a different load method after an error.

@paramsource The optional Wasm source. The default is the binary in this package.@returnsThe initialized Wasm exports. Use the re-exported Tunelet class for processors.
loadTunelet
();
const const engine: Tuneletengine = new new Tunelet(sample_rate: number, max_block_frames: number, config?: ProcessorConfig | null): Tunelet

Prepares a processor and its reusable PCM buffers.

sample_rate must be 44100 or 48000. max_block_frames must be an integer in 1..=4294967295. It is a limit for each call, and smaller blocks and empty blocks are also valid. If you omit config, the processor uses the default configuration. The constructor validates the settings before it allocates the processor or the PCM buffers. In TypeScript, use new Tunelet(sampleRate, maxBlockFrames, config).

Errors

The constructor throws if one of these conditions occurs:

  • The rate is not 44100 or 48000.
  • The block limit is not an integer or is out of range.
  • The configuration object is malformed or a field is missing.
  • The configuration is not valid.
  • The reservation of a PCM buffer fails.

The available Wasm memory must also have sufficient space for the DSP histories.

Tunelet
(44100, 4096);
const const left: Int16Array<ArrayBuffer>left = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(4096);
const const right: Int16Array<ArrayBuffer>right = new
var Int16Array: Int16ArrayConstructor
new (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array
(4096);
// Render a whole take block by block, then flush the tail. for (let let offset: numberoffset = 0; let offset: numberoffset < const pcm: Int16Array<ArrayBufferLike>pcm.Int16Array<ArrayBufferLike>.length: number

The length of the array.

length
; let offset: numberoffset += 4096) {
const const block: Int16Array<ArrayBufferLike>block = const pcm: Int16Array<ArrayBufferLike>pcm.Int16Array<ArrayBufferLike>.subarray(begin?: number, end?: number): Int16Array<ArrayBufferLike>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(let offset: numberoffset, var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.Math.min(...values: number[]): number

Returns the smaller of a set of supplied numeric expressions.

@paramvalues Numeric expressions to be evaluated.
min
(let offset: numberoffset + 4096, const pcm: Int16Array<ArrayBufferLike>pcm.Int16Array<ArrayBufferLike>.length: number

The length of the array.

length
));
const const n: numbern = const block: Int16Array<ArrayBufferLike>block.Int16Array<ArrayBufferLike>.length: number

The length of the array.

length
;
const engine: Tuneletengine.Tunelet.process(input: Int16Array, left: Int16Array, right: Int16Array, events?: BlockEvents | null): void

Processes mono input into left and right Int16Array buffers of equal length.

If the call is successful, it writes every output sample. Optional events apply before their frame, and their offsets obey the rules of TimedEvent[]. An empty block must have no events. A successful call cancels a drain, also for an empty block. The call uses the existing JavaScript and Rust PCM buffers again.

Errors

The method throws if the channel lengths are not equal. It also throws if the block is larger than maxBlockFrames or an event is not valid. The call checks the lengths and the block size before it decodes and validates the events. If an error occurs, the output buffers and the DSP state do not change.

process
(const block: Int16Array<ArrayBufferLike>block, const left: Int16Array<ArrayBuffer>left.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, const n: numbern), const right: Int16Array<ArrayBuffer>right.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, const n: numbern));
const writer: {
    write(left: Int16Array, right: Int16Array): void;
}
writer
.function write(left: Int16Array, right: Int16Array): voidwrite(const left: Int16Array<ArrayBuffer>left.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, const n: numbern), const right: Int16Array<ArrayBuffer>right.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, const n: numbern));
} const engine: Tuneletengine.Tunelet.beginDrain(): number

Starts a finite tail budget and returns its frame count as a number.

Call this method one time after the final input block. Another call starts the full budget again, but it does not clear the histories. See beginDrain for completion.

beginDrain
();
for (let let n: numbern = const engine: Tuneletengine.Tunelet.drain(left: Int16Array, right: Int16Array): number

Renders stereo tail frames into Int16Array buffers of equal length.

The method returns the number of rendered frames and fills the unused output with zeros. If the drain is not active or is complete, the method returns zero. Empty arrays do not use the budget. Completion resets the histories and the feedback in the final drain call, also when that call returns audio.

Errors

The method throws if the channel lengths are not equal or if a block is larger than maxBlockFrames. After an error, both output arrays and the drain state do not change.

drain
(const left: Int16Array<ArrayBuffer>left, const right: Int16Array<ArrayBuffer>right); let n: numbern > 0; let n: numbern = const engine: Tuneletengine.Tunelet.drain(left: Int16Array, right: Int16Array): number

Renders stereo tail frames into Int16Array buffers of equal length.

The method returns the number of rendered frames and fills the unused output with zeros. If the drain is not active or is complete, the method returns zero. Empty arrays do not use the budget. Completion resets the histories and the feedback in the final drain call, also when that call returns audio.

Errors

The method throws if the channel lengths are not equal or if a block is larger than maxBlockFrames. After an error, both output arrays and the drain state do not change.

drain
(const left: Int16Array<ArrayBuffer>left, const right: Int16Array<ArrayBuffer>right)) {
const writer: {
    write(left: Int16Array, right: Int16Array): void;
}
writer
.function write(left: Int16Array, right: Int16Array): voidwrite(const left: Int16Array<ArrayBuffer>left.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, let n: numbern), const right: Int16Array<ArrayBuffer>right.Int16Array<ArrayBuffer>.subarray(begin?: number, end?: number): Int16Array<ArrayBuffer>

Gets a new Int16Array view of the ArrayBuffer store for this array, referencing the elements at begin, inclusive, up to end, exclusive.

@parambegin The index of the beginning of the array.@paramend The index of the end of the array.
subarray
(0, let n: numbern));
} const engine: Tuneletengine.Tunelet.free(): voidfree(); // A custom binary location: // await loadTunelet(new URL("file:///opt/tunelet/tunelet_bg.wasm")); // From CommonJS: // const { loadTunelet, Tunelet } = await import("tunelet");

Concurrent calls share one initialization promise. If the load fails, the promise rejects and the cache is cleared, so you can retry.

Native Rust

The crate gives you the same processor with slices, Result errors and no runtime dependencies.

OperationMethods
PrepareSampleRate::try_from(hz), PrepareConfig::new(rate, max_block_frames), Processor::new(prep, config)
Processprocess (interleaved [[i16; 2]]), process_planar, process_mono_in_place. All take &[TimedEvent]
Configureconfig(), set_config(config)
Tailbegin_drain(), drain, drain_planar, drain_mono, tail_frames()
Inspectfeedback(), take_feedback(), latency_frames(), prepare_config()
Resetreset() keeps the configuration and the random stream
main.rs
use tunelet::{
    ControlEvent, PrepareConfig, Processor, ProcessorConfig, ReverbMode, SampleRate, TimedEvent,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let prep = PrepareConfig::new(SampleRate::Hz48000, 1024)?;

    let mut config = ProcessorConfig::default();
    config.reverb.enabled = true;
    config.reverb.mode = ReverbMode::MediumHall;
    config.reverb.mix = 0.25;

    let mut processor = Processor::new(prep, config)?;

    let input = [0i16; 1024];
    let mut left = [0i16; 1024];
    let mut right = [0i16; 1024];

    // A sample-accurate gain change before frame 512.
    let events = [TimedEvent { offset: 512, event: ControlEvent::OutputGain(0.8) }];
    processor.process_planar(&input, &mut left, &mut right, &events)?;

    let feedback = processor.feedback();
    println!("{:.1} Hz, voiced: {}", feedback.frequency_hz, feedback.voiced);

    // Flush the tail after the last block.
    processor.begin_drain();
    loop {
        let frames = processor.drain_planar(&mut left, &mut right)?;
        if frames == 0 {
            break;
        }
        // Use left[..frames] and right[..frames].
    }
    Ok(())
}

Build and lint:

cargo build --locked --release
cargo fmt --check
cargo clippy --locked --lib -- -D warnings

Render the full API documentation and open it:

cargo doc --locked --no-deps --open

Reference

Troubleshooting

The errors you are most likely to meet, and what they mean.

SymptomCause and fix
The Tunelet constructor throws a Wasm binding errorloadTunelet() has not resolved yet. Await it once before you construct a processor. Inside a worklet, use initSync with a precompiled module.
AudioWorkletNode fails with “node name is not defined in AudioWorkletGlobalScope”The worklet module threw while loading, usually because TextDecoder is missing there. Import the polyfill before tunelet/wasm.
setConfig or the constructor throwsA field is missing or out of range. Start from config() or defaultConfig() and check the ranges in the declarations. The processor keeps its previous settings.
process throws about eventsOffsets must be in range and non-decreasing, and an empty block takes no events. One bad event rejects the whole block.
process throws about lengthsInput, left and right must be equal in length and no longer than maxBlockFrames.
Output lags the input by about 33 ms, or the tail is missingThat is the 1,593-frame latency. Compensate in your host and drain at the end of the stream.
At 48 kHz, corrected notes are out of tune with the default configurationThe default chromatic table uses 44.1 kHz periods in 0.1.1. Set resynthesis.correction.enabled to false and then to true with setConfig. Or set a non-chromatic notes mask.
A pitch shift sounds like a chipmunkEnable resynthesis.formant so the vocal tract does not move while the pitch moves.
Clipping with many voicesChoir and harmony voices are summed. Lower outputGain or harmonyGain, or use fewer voices.
require("tunelet") failsThe package is ESM only. Use await import("tunelet").
Distorted audio in the browserCheck the Float32 to 16-bit conversion, and make sure the AudioContext runs at 44.1 or 48 kHz.