Audio File Processor (AFP)

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 WAV on disk, or samples already in memory. 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 two things a File does

A File does exactly two things, and each one reads the source from beginning to end:

  1. Stream. Deliver conditioned mono audio at your target rate, chunk by chunk. This is the same conditioning the live capture path applies, run over a recording instead of a microphone.
  2. Analyse. Score the whole recording for voice activity in one pass and return the per-window scores plus the merged speech segments. A live stream cannot do this, because it has no end.
One File, one operation. The two are alternatives, not a sequence. Each consumes the source once, so beginning one forecloses the other on that instance. To do both over the same recording, construct a second File. See the complete example.

Quick example

Open a WAV, 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

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.

Node.js also has a global 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.

Opening a source

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 WAV on disk, blocking is fine decibri.File(path) new File(path)
A WAV 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)

What a File accepts

The path constructors read WAV. Two encodings are supported, and both are accepted inside the WAVE_FORMAT_EXTENSIBLE container as well as the plain one, which matters because several common encoders emit extensible by default:

Anything else, 8-bit and 24-bit PCM among them, is rejected at construction with the message invalid WAV file: unsupported encoding; 16-bit PCM or 32-bit float required. For those, and for any other codec, decode the audio yourself and pass the samples to the buffer constructor: the conditioning behaves identically either way.

Two more things worth knowing before you rely on them. Multi-channel WAVs are accepted and downmixed to mono, where the Microphone rejects multi-channel input outright. And a header-only WAV is valid rather than an error: it constructs, reports its rates, yields no chunks, and analyses to an empty report.

The conditioning chain

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,
)

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.

Chunks and rates

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 WAV header or taken from the value you passed to the buffer constructor. They differ whenever the recording is resampled.

Rate conversion and denoise both lengthen the output. The resampler's internal state is flushed at the end of the stream and those samples are delivered rather than discarded, and the denoise stage does the same. A one-second recording resampled to a different rate therefore yields slightly more than one second of samples. Code that allocates an output buffer from 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.

Voice activity detection

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

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)

Whole-recording analysis

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:

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)

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.

Complete example

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)

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.

Per-language detail

The full constructor surface, the metadata iteration, and the error tables live on the binding reference pages:

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.