The Audio File Processor (AFP) is decibri's offline audio source. It runs a recording through the same conditioning chain the live microphone uses, and, because a recording has an end, it can also score the whole file for speech in one pass. The class is called File, in both Python and Node.js.
Where a Microphone hands you conditioned audio as it arrives, a File hands you conditioned audio from something you already have: a recording on disk, or samples already in memory. It also writes what it produced, so a conditioned recording can go straight back out to a file. The options are the same options, with the same names and the same ranges, and the chunks come out in the same shape. Like the rest of the engine it is keyless and local: no API key, no account, no network call.
The path constructors read four containers: WAV, AIFF, AIFF-C and FLAC. Every encoding below opens through the same File(path) call, so nothing about the call changes with the format. Every conditioning option works on every one of them, and analyze() returns the same scores and the same segments for the same recording whichever container carried it.
| Container | Encodings |
|---|---|
| WAV | 8-bit unsigned, 16-bit, 24-bit and 32-bit integer PCM, 32-bit and 64-bit IEEE float, mu-law and A-law. Also WAVE_FORMAT_EXTENSIBLE, which several common encoders emit by default, and RF64. |
| AIFF and AIFF-C | 8-bit, 16-bit, 24-bit and 32-bit integer PCM, 32-bit and 64-bit IEEE float, mu-law and A-law, plus AIFF-C's little-endian sowt alongside the big-endian forms. |
| FLAC | Bit depths 4 through 32. |
save() writes three of those four: WAV, AIFF and FLAC, always as 16-bit PCM mono. See Saving the result.
The container is identified from the file's first twelve bytes rather than from its name, so a FLAC called clip.wav opens as a FLAC. Writing goes the other way: save() takes the container from the extension you chose. The asymmetry is deliberate, and it is worth holding both halves in mind at once. On a read there is a file to inspect; on a write there is only the name the caller supplied.
MP3, AAC, m4a, Ogg Vorbis, Opus, WMA and every ADPCM variant are not supported, and support for them is not planned: decoding them needs codecs decibri does not carry. For audio in one of those, decode it yourself and pass the samples to the buffer constructor; the conditioning behaves identically either way.
Multi-channel recordings are accepted and downmixed to mono, where the Microphone rejects multi-channel input outright.
Two structural cases are refused rather than tolerated. A WAV carrying a fmt chunk and no data chunk raises AudioFileMalformed, because it promises audio it does not contain; a data chunk of length zero is a legitimate empty recording and still opens, reports its rates, yields no chunks and analyses to an empty report. And a WAV whose declared data length is not a whole number of frames raises AudioFileTruncated instead of quietly delivering audio a fraction of a frame short of its own declaration. That second case is not the interrupted download, which declares more bytes than it holds and already failed to open; it is what a repair tool or an unfinalised writer leaves behind.
A File does exactly three things, and each one reads the source from beginning to end:
save() consumes the source exactly as analyze() does, and raises the same engaged and consumed errors when it cannot. To do two of them over the same recording, construct a second File. See the complete example.
Open a recording, turn on the conditioning stages you want, and read conditioned chunks:
import decibri
with decibri.File("clip.wav", denoise="fastenhancer-t", agc=-18) as file:
for chunk in file:
handle(chunk) # conditioned int16 PCM bytes
const { File } = require('decibri');
const file = await File.open('clip.wav', { denoise: 'fastenhancer-t', agc: -18 });
for await (const chunk of file) {
handle(chunk); // Buffer of conditioned Int16 LE PCM
}
file.close();
Leave an option unset to keep that stage off. With every stage off, a File simply reads the recording, downmixes it to mono, and resamples it to your target rate.
File. The web File API defines one. Import decibri's explicitly with const { File } = require('decibri'), or reference it as decibri.File, so the two do not shadow each other.
There are three ways in, and the right one depends on where the audio is and whether you are on an event loop.
| Situation | Python | Node.js |
|---|---|---|
| A recording on disk, blocking is fine | decibri.File(path) |
new File(path) |
| A recording on disk, inside async code | await decibri.AsyncFile.open(path) |
await File.open(path) |
| Samples already in memory | decibri.File.buffer(samples, input_rate=...) |
File.buffer(samples, { inputRate: ... }) |
Two spellings are easy to mix up. In Python, File.open(path) is a plain alternate spelling of File(path) and returns the same object; the async form is a separate class, AsyncFile. In Node.js, File.open() is the async factory and reads the file off the event loop, while File.buffer() is synchronous because no I/O is involved.
The buffer constructor takes mono samples in the range -1.0 to 1.0: a list of floats or a one-channel NumPy array with a floating dtype in Python, a Float32Array in Node.js. Raw samples carry no header, so their native rate has to be passed explicitly on input_rate (inputRate in Node.js). It is required, and range-checked from 1,000 to 384,000 exactly as the target rate is.
import decibri
# samples: mono floats in -1.0 to 1.0, at their own native rate
file = decibri.File.buffer(samples, input_rate=48000, sample_rate=16000)
for chunk in file:
handle(chunk)
const { File } = require('decibri');
// samples: a Float32Array of mono samples in -1.0 to 1.0, at their own rate
const file = File.buffer(samples, { inputRate: 48000, sampleRate: 16000 });
file.on('data', (chunk) => handle(chunk));
A File takes the same five conditioning options as a Microphone, with the same names, the same ranges, and the same fixed order: DC removal → denoise → high-pass → AGC → limiter. Every stage is off by default. The Audio Capture Engine page documents what each stage does and how to choose its value; everything there applies to a File unchanged.
file = decibri.File(
"clip.wav",
sample_rate=16000,
dc_removal=True,
denoise="fastenhancer-t",
highpass=100,
agc=-18,
limiter=-1.0,
)
const file = await File.open('clip.wav', {
sampleRate: 16000,
dcRemoval: true,
denoise: 'fastenhancer-t',
highpass: 100,
agc: -18,
limiter: -1.0,
});
As on the microphone, voice activity detection reads the signal before the conditioning chain, so turning on enhancement does not change what counts as speech. It only changes the audio you receive.
A full chunk is 1,600 samples at the target rate, in both bindings. That is 3,200 bytes as int16 and 6,400 bytes as float32. The final chunk is shorter whenever the total does not divide evenly.
The count is fixed and the rate is not, so the chunk duration moves with the target rate: 100 ms at 16 kHz, 200 ms at 8 kHz, and about 33 ms at 48 kHz. Size buffers and reason about latency from the duration, not the count.
Two rates are visible on the instance. sample_rate (sampleRate) is the target rate every delivered chunk carries. input_rate (inputRate) is the source's own rate, read from the file's header or taken from the value you passed to the buffer constructor. They differ whenever the recording is resampled.
duration × rate will come up short. The one case with no tail is the identity case: source rate equal to target rate, with denoise off, gives exactly the input sample count.
In Node.js, the total number of bytes is exact but the number of chunks depends on how you consume the stream: a for await loop and a 'data' listener over the same recording produce the same total in a different number of pieces, because Readable coalesces its internal buffer. Treat the total as exact and the chunk count as a property of the consumption style, not of decibri.
VAD is opt-in through the vad option, exactly as on the Microphone: pass "silero" or "energy" for the defaults, or a config object to tune the threshold and the holdoff. Without it, a File just conditions audio.
While you are streaming, Python exposes the speaking state as the is_speaking property and the raw score as vad_score. Node.js emits 'speech' and 'silence' events and exposes the raw score as vadScore.
import decibri
with decibri.File("clip.wav", vad="silero") as file:
for chunk in file:
if file.is_speaking:
send_to_stt(chunk) # file.vad_score holds the raw score
const { File } = require('decibri');
const file = await File.open('clip.wav', { vad: 'silero' });
let speaking = false;
file.on('speech', () => { speaking = true; });
file.on('silence', () => { speaking = false; });
file.on('data', (chunk) => {
if (speaking) {
sendToStt(chunk); // file.vadScore holds the raw score
}
});
The holdoff on a File is measured in file time, not wall-clock time. A recording processes faster than real time, so a wall-clock timer would collapse the reported speech timing; measuring it in sample positions means a file reports the same state sequence a live stream of the identical audio would.
Three different things carry a speech signal here, and they are not interchangeable:
| What you want | What to use |
|---|---|
| The speech regions of a whole recording | analyze().segments |
| Per-window scores across a whole recording | analyze().scores |
| The speech score while streaming chunks | vad_score (Python), vadScore (Node.js) |
| The debounced speech state while streaming | is_speaking (Python), the 'speech' and 'silence' events (Node.js) |
analyze() runs the recording once and returns a report of where the speech is. It requires vad="silero": a File built without a VAD configuration is refused with analysis requires VAD; construct the File with a vad configuration, and energy mode has no whole-recording analysis. analyse() is the same call under the other spelling.
The report has exactly two parts, both in seconds of file time:
scores: one entry per analysis window, carrying start, end, the window's speech score, and whether that score met the threshold. Windows tile the recording from zero, contiguous and non-overlapping, 0.032 seconds each. A trailing remainder shorter than one window is left unscored.segments: the merged speech regions, each with a start and an end. Consecutive speech windows whose silence gaps fall within the holdoff collapse into one segment, and a segment ends at its last speech window rather than at the holdoff expiry.import decibri
report = decibri.File("clip.wav", vad="silero").analyze()
for segment in report.segments:
print(f"speech from {segment.start:.3f}s to {segment.end:.3f}s")
for window in report.scores:
print(window.start, window.end, window.vad_score, window.is_speech)
const { File } = require('decibri');
const file = await File.open('clip.wav', { vad: 'silero' });
const report = await file.analyze();
for (const segment of report.segments) {
console.log(`speech from ${segment.start}s to ${segment.end}s`);
}
for (const window of report.scores) {
console.log(window.start, window.end, window.vadScore, window.isSpeech);
}
The threshold and the holdoff shape the result: raising the threshold trims the segment edges, and a long holdoff merges neighbouring segments into one. Both are set on the vad config object at construction, so changing them means constructing a new File.
save() is the third pass. It runs the recording once through whatever conditioning the File was built with, whole, and writes the result to a path you name. Like the other two passes it consumes the source, so a File that has saved cannot then be streamed or analysed.
import decibri
file = decibri.File("noisy.wav", denoise="fastenhancer-t", agc=-18, limiter=-1.0)
report = file.save("clean.flac")
print(report.clipped_samples, report.non_finite_samples)
const { File } = require('decibri');
const file = await File.open('noisy.wav', {
denoise: 'fastenhancer-t',
agc: -18,
limiter: -1.0,
});
const report = await file.save('clean.flac');
console.log(report.clippedSamples, report.nonFiniteSamples);
The container comes from the extension: .wav, .aiff, .aif, .aifc and .flac, matched without regard to case. An extension decibri does not recognise, and a path carrying no extension at all, are both refused with AudioFormatUnsupported rather than defaulted to something. One name is not quite what it looks like: .aifc writes a plain AIFF, not an AIFF-C. To decide the container yourself, set format to "wav", "aiff" or "flac", which overrides the extension entirely.
What comes out is 16-bit PCM, mono, at the File's target rate, in all three containers. No option changes that. compression sets the FLAC compression level, 0 through 8, default 5: higher levels search harder for a smaller file, and every level decodes to identical audio. It applies to FLAC only and is ignored for WAV and AIFF, and a value outside the range is refused.
The call returns a report of what the write did to the samples, carrying two counts. clipped_samples (clippedSamples in Node.js) counts finite samples that fell outside full scale and were clamped back into the -1.0 to 1.0 range. That count is the reason to read the report at all: AGC without a limiter can push a signal past full scale, and 16-bit PCM cannot hold it. Rather than clip silently, decibri clamps and tells you how often, which makes the fix visible: turn the limiter on and save again. non_finite_samples (nonFiniteSamples) counts samples that were NaN or infinite and were repaired on the way out, a NaN written as silence and an infinity as full scale.
Reading four containers and writing three does mean some conversions work. Open a FLAC, save a WAV, and you have converted it. That falls out of saving conditioned output rather than being a feature with a surface of its own, and the shape of the result says as much: 16-bit PCM mono at the File's target rate, through whatever conditioning the File carries, with no encoder to choose. What is not coming is a codec decibri does not carry. MP3, AAC and Ogg are not supported and are not planned.
This is the two-pass shape the single-pass rule leads to: analyse one File to find the speech, then construct a second File over the same recording to stream the conditioned audio.
import decibri
# Pass one: where is the speech? This File is consumed by the analysis.
report = decibri.File("clip.wav", vad="silero").analyze()
# Pass two: a second File over the same recording, conditioned and streamed.
# chunk.timestamp is the chunk's position in seconds of file time, which is
# the same clock report.segments is measured on.
with decibri.File(
"clip.wav",
denoise="fastenhancer-t",
agc=-18,
limiter=-1.0,
) as file:
for chunk in file.iter_with_metadata():
if any(s.start <= chunk.timestamp <= s.end for s in report.segments):
send_to_stt(chunk.data)
const { File } = require('decibri');
// Pass one: where is the speech? This File is consumed by the analysis.
const scan = await File.open('clip.wav', { vad: 'silero' });
const report = await scan.analyze();
// Pass two: a second File over the same recording, conditioned and streamed.
const file = await File.open('clip.wav', {
denoise: 'fastenhancer-t',
agc: -18,
limiter: -1.0,
});
let position = 0; // seconds of file time, the clock report.segments uses
for await (const chunk of file) {
const inSpeech = report.segments.some(
(s) => s.start <= position && position <= s.end
);
if (inSpeech) {
sendToStt(chunk);
}
position += chunk.length / 2 / file.sampleRate; // int16: 2 bytes per sample
}
file.close();
Constructing a second File over the same source is the supported way to do both. Neither pass waits on real time, so the second one costs a second read of the recording rather than a second playback of it.
The full constructor surface, the metadata iteration, and the error tables live on the binding reference pages:
File and AsyncFile classes, including read_with_metadata(), iter_with_metadata(), as_ndarray, save() with its SaveReport, and the exception surface.File stream, its FileOptions surface, save() with its SaveReport, the speech events, the AudioWriter sink, and the error classes with their code values.AFP is a native stage of the engine, so it is available in the Python and Node.js builds but not in the browser build. See the Browser API.