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.
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.
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.
Call await loadTunelet() once. The import of the package does not load the binary.
Build a configuration from defaultConfig(), or omit it to use the defaults.
Construct new Tunelet(sampleRate, maxBlockFrames, config). Both numbers are fixed for the lifetime of the processor.
Allocate Int16Array buffers once and call process(input, left, right) for every block.
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.awaitfunction 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.constconst 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.
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.
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.constconst input: Int16Array<ArrayBuffer>input = new
var Int16Array: Int16ArrayConstructornew (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array(1024);constconst left: Int16Array<ArrayBuffer>left = new
var Int16Array: Int16ArrayConstructornew (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array(1024);constconst right: Int16Array<ArrayBuffer>right = new
var Int16Array: Int16ArrayConstructornew (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.
Stage
Config key
What it does
Default
Punch
punch
Input gain, transient emphasis and arctangent saturation
off
Warm
warm
Asymmetric saturation followed by a 50 Hz high-pass
off
Resynthesis
resynthesis
Pitch tracking, correction, pitch shift, choir or harmony voices, formants
chromatic correction, one voice
Alien
alien
Reverse playback of recent audio in fading loops
off
Reverb
reverb
Echo and reverberation networks
off
Output gain
outputGain
Linear gain 0..=64, then clipping to 16 bits
1
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.
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.constconst 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.
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.
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 layout
Process
Tail
Planar left and right
process / process_planar
drain / drain_planar
Interleaved [[i16; 2]] frames (Rust)
process
drain
Mono, in place
processMono / process_mono_in_place
drainMono / 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.
After the last input block, call beginDrain() once. It returns the tail budget in frames. tailFrames() reports the same budget for the current configuration.
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.
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.constconst 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.
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.
Start from defaultConfig(), or from config(), which returns an independent copy of the active settings.
Change the fields you need. Objects use camelCase names and enums are string literals, so your editor completes them.
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.constconst 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.
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 downconst 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.
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..=64const 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.
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.
Group
Fields worth knowing
Range
resynthesis
pitchRatio
0.25..=4 (0.5 is one octave down)
resynthesis.correction
enabled, notes, retuneMs
12-entry mask C..B; 0..=400 ms half-life
resynthesis.formant
width, stretch, aspirationMix
0.5..=2, 0.5..=2, 0..=1
resynthesis
mode, choirVoices, harmony.*
"choir" or "harmony"; 1..=16 voices
punch, warm
gainDb, impact, driveDb
-96..=12 dB, 0..=1, 0..=6 dB
reverb
mode, mix, diffusion, decay, brightnessHz
six networks; 0..=1; 80..=20000 Hz
outputGain
0..=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.
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 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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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 type
Payload
Effect
configure
ProcessorConfig
Replace the whole configuration
punch, warm, resynthesis, alien, reverb
That stage’s config object
Replace every field of one stage
outputGain
number, 0..=64
Set the final gain
midi
MidiEvent
Harmony note control
correctionMidi
MidiEvent
Correction keyboard
reset
none
Same 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.
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
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.
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.
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.
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.
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.
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.
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.
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";awaitfunction 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.
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);constconst input: Int16Array<ArrayBuffer>input = new
var Int16Array: Int16ArrayConstructornew (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array(4096); // Mono 16-bit PCMconstconst left: Int16Array<ArrayBuffer>left = new
var Int16Array: Int16ArrayConstructornew (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array(4096);constconst right: Int16Array<ArrayBuffer>right = new
var Int16Array: Int16ArrayConstructornew (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
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.
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.
Create the AudioContext at 44,100 or 48,000 Hz and register your worklet module.
Serve tunelet_bg.wasm as a static asset and compile it on the main thread with WebAssembly.compileStreaming. The worklet scope has no fetch().
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.
Pass the compiled module through processorOptions, call initSync({ module }) in the processor constructor, then create the Tunelet.
Convert each 128-frame Float32 block to 16-bit PCM, process it, and convert back.
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().constconst module: WebAssembly.Modulemodule = awaitWebAssembly.function WebAssembly.compileStreaming(source: Response | PromiseLike<Response>): Promise<WebAssembly.Module>
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.
The Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.
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.
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.
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.
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.
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.
The destination property of the BaseAudioContext interface returns an AudioDestinationNode representing the final destination of all audio in the context.
// AudioWorkletGlobalScope has no TextDecoder or TextEncoder; the Wasm bindings need both.constconst scope: anyscope =module globalThisglobalThisas 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 ""; letlet out: stringout= ""; for (letlet i: numberi= 0; let i: numberi<bytes: Uint8Array<ArrayBufferLike>bytes.Uint8Array<ArrayBufferLike>.length: number
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.
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.
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.
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.
@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 firstimport {
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.
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 readonlyVocalProcessor.pcmIn: Int16Array<ArrayBuffer>pcmIn= new
var Int16Array: Int16ArrayConstructornew (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array(128); private readonlyVocalProcessor.pcmL: Int16Array<ArrayBuffer>pcmL= new
var Int16Array: Int16ArrayConstructornew (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array(128); private readonlyVocalProcessor.pcmR: Int16Array<ArrayBuffer>pcmR= new
var Int16Array: Int16ArrayConstructornew (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.awaitfunction 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.
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);constconst left: Int16Array<ArrayBuffer>left = new
var Int16Array: Int16ArrayConstructornew (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array(4096);constconst right: Int16Array<ArrayBuffer>right = new
var Int16Array: Int16ArrayConstructornew (length: number) => Int16Array<ArrayBuffer> (+6 overloads)
Int16Array(4096);// Render a whole take block by block, then flush the tail.for (letlet offset: numberoffset= 0; let offset: numberoffset<const pcm: Int16Array<ArrayBufferLike>pcm.Int16Array<ArrayBufferLike>.length: number
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.
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 (letlet 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.
The errors you are most likely to meet, and what they mean.
Symptom
Cause and fix
The Tunelet constructor throws a Wasm binding error
loadTunelet() 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 throws
A 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 events
Offsets must be in range and non-decreasing, and an empty block takes no events. One bad event rejects the whole block.
process throws about lengths
Input, 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 missing
That 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 configuration
The 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 chipmunk
Enable resynthesis.formant so the vocal tract does not move while the pitch moves.
Clipping with many voices
Choir and harmony voices are summed. Lower outputGain or harmonyGain, or use fewer voices.
require("tunelet") fails
The package is ESM only. Use await import("tunelet").
Distorted audio in the browser
Check the Float32 to 16-bit conversion, and make sure the AudioContext runs at 44.1 or 48 kHz.