Audio processing for TypeScript. Chainable, immutable, serverless-ready.
Everything revolves around AudioTrack — an immutable object holding PCM audio data. You create one from a buffer, transform it with chainable methods, and export the result.
import { AudioTrack } from "neiro";
const track = await AudioTrack.fromBuffer({ buffer: mp3Buffer });
const result = track
.normalize({ target: -14 })
.trimSilence()
.fadeOut({ ms: 10 });
const output = result.toMp3({ bitrate: 128 });Methods that take options use a single object argument. No-arg methods like reverse(), toMono(), and toStereo() stay no-arg.
Every transform returns a new AudioTrack. The original is never mutated.
Decode a compressed audio buffer (MP3, WAV, OGG, FLAC) into an AudioTrack.
const track = await AudioTrack.fromBuffer({
buffer: Buffer;
}): Promise<AudioTrack>This is async because decoding compressed audio requires parsing frame headers, Huffman tables, etc. Format is auto-detected from the buffer contents.
Raw PCM is not auto-detected here. Raw PCM has no container header, so use AudioTrack.fromPcm() when you have s16le bytes.
const track = await AudioTrack.fromBuffer({ buffer });Create an AudioTrack from raw interleaved PCM bytes.
const track = AudioTrack.fromPcm({
buffer: Buffer;
sampleRate: number;
channels?: number; // default: 1
format?: "s16le"; // default: "s16le"
}): AudioTrackv1 supports only signed 16-bit little-endian PCM ("s16le"), with mono or stereo channel layouts. The PCM is assumed to be interleaved.
const track = AudioTrack.fromPcm({
buffer: pcm,
sampleRate: 48000,
channels: 1,
format: "s16le",
});
const wav = track.toWav();Create an AudioTrack from already-decoded normalized sample arrays. Use this when you already have floating-point PCM data.
const track = AudioTrack.fromChannels({
channels: Float32Array[]; // One Float32Array per channel
sampleRate: number;
}): AudioTrackSamples must be normalized to the -1.0 to 1.0 range.
// Mono track from raw samples
const mono = AudioTrack.fromChannels({
channels: [samples],
sampleRate: 44100,
});
// Stereo track
const stereo = AudioTrack.fromChannels({
channels: [left, right],
sampleRate: 48000,
});Generate a silent track. Useful as a building block with concat() and mix().
const gap = AudioTrack.silence({
durationMs: number;
sampleRate?: number; // default: 44100
channels?: number; // default: 1
}): AudioTrack// 500ms of silence
const gap = AudioTrack.silence({ durationMs: 500 });
// 1 second of stereo silence at 48kHz
const gap = AudioTrack.silence({
durationMs: 1000,
sampleRate: 48000,
channels: 2,
});Read-only properties on every AudioTrack instance.
| Property | Type | Description |
|---|---|---|
duration |
number |
Duration in seconds |
sampleRate |
number |
Sample rate in Hz (e.g., 44100, 48000) |
channels |
number |
Channel count |
length |
number |
Total samples per channel |
const track = await AudioTrack.fromBuffer({ buffer: mp3Buffer });
track.duration; // 4.2
track.sampleRate; // 44100
track.channels; // 2
track.length; // 185220Methods that analyze the audio without modifying it. These take no arguments.
Measure integrated loudness per ITU-R BS.1770-4 / EBU R128.
track.loudness(): number // LUFSReturns the integrated loudness in LUFS (Loudness Units relative to Full Scale). Returns -Infinity for silence or audio shorter than 400ms (the minimum block size for LUFS measurement).
The measurement applies K-weighting (a frequency curve that models human loudness perception) and dual gating (absolute gate at -70 LUFS, relative gate at -10 LU below the ungated mean).
track.loudness(); // -22.3Measure true peak level using 4x oversampling per ITU-R BS.1770-4.
track.truePeak(): number // dBTP (decibels true peak)True peak detection uses polyphase FIR interpolation to find intersample peaks — values that occur between samples when a DAC reconstructs the analog signal. This is more accurate than simply finding the loudest sample.
Returns the maximum true peak across all channels in dBTP.
track.truePeak(); // -0.8Measure RMS (Root Mean Square) level.
track.rms(): number // dBRMS represents the average power of the signal — a rough proxy for perceived loudness (less accurate than LUFS, but faster to compute and doesn't require a minimum duration).
Returns the RMS level in dB across all channels.
track.rms(); // -18.5Access raw PCM samples for a specific channel.
track.getChannel({ index: number }): Float32ArrayReturns a copy of the channel data (the track remains immutable).
0= left (or mono)1= right
Throws if the index is out of range.
const leftSamples = track.getChannel({ index: 0 });
const rightSamples = track.getChannel({ index: 1 });Every transform returns a new AudioTrack. They can be chained. Methods that take options use a single object argument.
Apply gain (volume adjustment) in decibels.
track.gain({ db: number }): AudioTrackPositive values boost, negative values attenuate. No clipping protection — use normalize() if you need peak limiting.
track.gain({ db: 6 }); // +6 dB (roughly doubles perceived loudness)
track.gain({ db: -3 }); // -3 dB (roughly halves power)Normalize loudness to a target LUFS with true peak limiting.
track.normalize({
target?: number; // Target LUFS (default: -14)
peakLimit?: number; // True peak ceiling in dBTP (default: -1.5)
}): AudioTrackThis is the main workhorse for loudness normalization:
- Measures current integrated loudness (LUFS)
- Calculates the gain needed to reach
target - Applies gain
- If true peak exceeds
peakLimit, applies stereo-matched gain reduction
The stereo-matched limiting is important: both channels get the same gain reduction so the stereo image isn't distorted.
Returns the original track unchanged if loudness is -Infinity (silence/too short).
// Broadcast standard (-14 LUFS, -1.5 dBTP ceiling)
track.normalize();
// Quieter background audio
track.normalize({ target: -20 });
// Tighter peak control
track.normalize({ target: -14, peakLimit: -1.0 });Remove leading and trailing silence.
track.trimSilence({
thresholdDb?: number; // Silence threshold in dB RMS (default: -30)
headMs?: number; // Buffer to keep before content (default: 10)
tailMs?: number; // Buffer to keep after content (default: 50)
}): AudioTrackUses fixed 10ms RMS analysis windows internally instead of sample-level detection. Each window is analyzed per channel, and the loudest channel RMS decides whether that window counts as content.
- Leading trim: Scans forward with the configured threshold. Keeps
headMsof buffer before the first loud window for a natural attack. - Trailing trim: Scans backward with the configured threshold. Keeps
tailMsof buffer after the last loud window for natural decay.
Returns the original track unchanged if no analysis window crosses the threshold.
// Sensible defaults
track.trimSilence();
// More aggressive leading trim
track.trimSilence({ thresholdDb: -20, headMs: 5 });
// Keep more tail for reverb
track.trimSilence({ tailMs: 200 });Apply a linear fade-in from silence.
track.fadeIn({ ms: number }): AudioTrackRamps gain from 0 to 1 over the specified duration. Use it when you need a short cleanup fade after an aggressive trim or when you want an audible fade-in effect.
track.fadeIn({ ms: 5 }); // Optional short cleanup fade
track.fadeIn({ ms: 500 }); // 500ms fade-in (artistic)Apply a linear fade-out to silence.
track.fadeOut({ ms: number }): AudioTrackRamps gain from 1 to 0 over the specified duration at the end of the track.
track.fadeOut({ ms: 10 }); // 10ms fade-out (click prevention)
track.fadeOut({ ms: 2000 }); // 2 second fade-out (song ending)Extract a segment of the track.
track.slice({
startMs: number;
endMs?: number; // Defaults to end of track
}): AudioTrackReturns a new track containing only the audio between startMs and endMs.
// First 2 seconds
track.slice({ startMs: 0, endMs: 2000 });
// Everything after the first second
track.slice({ startMs: 1000 });
// A 500ms segment starting at 3 seconds
track.slice({ startMs: 3000, endMs: 3500 });Explicitly resample the track to a new sample rate.
track.resample({
sampleRate: number;
}): AudioTrackUses linear interpolation in v1. Preserves channel count and preserves duration as closely as possible. Resampling to the same sample rate still returns a new copied track.
Throws resample sampleRate must be a finite positive number if sampleRate is not finite or is less than or equal to zero.
const resampled = track.resample({ sampleRate: 48000 });Downmix the track to mono. Takes no arguments.
track.toMono(): AudioTrackAverages all channels equally per frame. Works for mono, stereo, and multi-channel inputs. Mono input still returns a new copied mono track.
const mono = track.toMono();Convert the track to stereo. Takes no arguments.
track.toStereo(): AudioTrack- Mono input: duplicates the mono channel into left and right.
- Stereo input: returns a new copied stereo track.
- More than 2 channels: first downmixes to mono with the same averaging rule as
toMono(), then duplicates into stereo.
const stereo = track.toStereo();Join two tracks end-to-end.
track.concat({ other: AudioTrack }): AudioTrackThe tracks must have the same sample rate and channel count. Throws if they don't match. concat() does not resample or convert channels implicitly.
const intro = await AudioTrack.fromBuffer({ buffer: introMp3 });
const body = await AudioTrack.fromBuffer({ buffer: bodyMp3 });
const full = intro.concat({ other: body });Throws Cannot concat tracks with different sample rates or Cannot concat tracks with different channel counts if the tracks are incompatible.
Mix (overlay) another track on top of this one.
track.mix({
other: AudioTrack;
gainDb?: number; // Gain applied to `other` before mixing (default: 0)
}): AudioTrackAdds the samples together. The output length is the longer of the two tracks. The tracks must have the same sample rate and channel count. mix() does not resample or convert channels implicitly.
// Layer background ambience under dialogue
const dialogue = await AudioTrack.fromBuffer({ buffer: dialogueMp3 });
const ambience = await AudioTrack.fromBuffer({ buffer: ambienceMp3 });
const mixed = dialogue.mix({ other: ambience, gainDb: -6 });Throws Cannot mix tracks with different sample rates or Cannot mix tracks with different channel counts if the tracks are incompatible.
Reverse the audio. Takes no arguments.
track.reverse(): AudioTrackconst reversed = track.reverse();Change playback speed by resampling. Does not preserve pitch.
track.speed({ rate: number }): AudioTrackrate > 1 = faster and higher pitched. rate < 1 = slower and lower pitched. Uses linear interpolation for resampling.
The output sample rate stays the same — the duration changes.
track.speed({ rate: 2 }); // Double speed, half duration, octave up
track.speed({ rate: 0.5 }); // Half speed, double duration, octave down
track.speed({ rate: 1.5 }); // 1.5x speedEncode the track to MP3.
track.toMp3({
bitrate?: number; // kbps (default: 128)
}): Bufferconst mp3 = track.toMp3(); // 128 kbps
const hq = track.toMp3({ bitrate: 320 }); // 320 kbpsEncode the track to WAV (16-bit PCM). Takes no arguments.
track.toWav(): BufferWAV is lossless and trivial to parse. Use this when you need an interchange format or when quality matters more than file size.
const wav = track.toWav();Get the raw PCM data and metadata. Takes no arguments.
track.toPcm(): {
channels: Float32Array[];
sampleRate: number;
}Returns copies of the internal channel arrays. Use this when you want to feed the data into another system.
const { channels, sampleRate } = track.toPcm();
// channels[0] = left/mono, channels[1] = right (if stereo)const sfx = await AudioTrack.fromBuffer({ buffer: raw });
const processed = sfx
.normalize({ target: -14, peakLimit: -1.5 })
.trimSilence({ thresholdDb: -30, headMs: 10, tailMs: 50 })
.fadeOut({ ms: 10 });
const output = processed.toMp3({ bitrate: 128 });const track = AudioTrack.fromPcm({
buffer: pcm,
sampleRate: 48000,
channels: 1,
format: "s16le",
});
const wavBuffer = track.toWav();This is the primary pcm_48000 path for short one-shot assets. Keep the track at 48 kHz end-to-end to avoid unnecessary conversion.
const music = await AudioTrack.fromBuffer({ buffer: raw });
const processed = music
.normalize({ target: -20 })
.fadeIn({ ms: 500 })
.fadeOut({ ms: 2000 });
const output = processed.toMp3({ bitrate: 192 });const normalized = track
.toStereo()
.resample({ sampleRate: 48000 });
const padded = AudioTrack.silence({
durationMs: 500,
sampleRate: normalized.sampleRate,
channels: normalized.channels,
}).concat({ other: normalized });For inputs with more than 2 channels, toStereo() uses a simple downmix-to-mono-then-duplicate rule.
const beep = await AudioTrack.fromBuffer({ buffer: beepMp3 });
const gap = AudioTrack.silence({
durationMs: 300,
sampleRate: beep.sampleRate,
channels: beep.channels,
});
const sequence = beep
.concat({ other: gap })
.concat({ other: beep })
.concat({ other: gap })
.concat({ other: beep });
const output = sequence.toWav();const track = await AudioTrack.fromBuffer({ buffer });
console.log(`Loudness: ${track.loudness()} LUFS`);
console.log(`True Peak: ${track.truePeak()} dBTP`);
console.log(`RMS: ${track.rms()} dB`);
console.log(`Duration: ${track.duration}s`);neiro throws standard Error instances with descriptive messages:
| Error | When |
|---|---|
"Unsupported sample rate for K-weighting: {rate}Hz" |
LUFS measurement with a sample rate other than 44100 or 48000 |
"Sample rate must be positive" |
fromPcm() with sampleRate <= 0 |
"PCM buffer length must be divisible by frame size" |
fromPcm() with non-frame-aligned PCM input |
"resample sampleRate must be a finite positive number" |
resample() with 0, negative, NaN, or Infinity |
"Cannot concat tracks with different sample rates" |
concat() with mismatched sample rates |
"Cannot concat tracks with different channel counts" |
concat() with mismatched channel counts |
"Cannot mix tracks with different sample rates" |
mix() with mismatched sample rates |
"Cannot mix tracks with different channel counts" |
mix() with mismatched channel counts |
"Channel index out of range" |
getChannel() with an invalid index |
"Speed rate must be positive" |
speed() with zero or negative rate |
For fromBuffer(), decoding errors from the underlying codec are passed through. Raw PCM should go through fromPcm(), not fromBuffer().
| Parameter | Default | Standard |
|---|---|---|
normalize.target |
-14 LUFS |
EBU R128 foreground |
normalize.peakLimit |
-1.5 dBTP |
EBU R128 true peak |
trimSilence.thresholdDb |
-30 dB RMS |
Windowed silence threshold |
trimSilence.headMs |
10 ms |
Natural attack preservation |
trimSilence.tailMs |
50 ms |
Natural decay preservation |
fromPcm.channels |
1 |
Mono raw PCM input |
fromPcm.format |
"s16le" |
Signed 16-bit little-endian |
toMp3.bitrate |
128 kbps |
Good quality/size balance |