Blog/Check WAV audio for silence before transcription
OverviewAll posts
Tutorial

Check WAV audio for silence before transcription

Detect empty or all-zero PCM WAV audio with a bounded Python check, inspect stereo channels separately, and avoid confusing signal with speech.

VTornado API team
Covered in this article
Inspect channels without mixing them
Interpret the result conservatively
Place the check after delivery and before transcription
6 min reading time
Published November 8, 2026
VProduct guides by Velys Software

Before sending a WAV file for transcription, distinguish three cases: no audio frames, frames containing only zero-valued samples, and a nonzero signal. A file can have a valid header and a positive duration while every sample is zero. Conversely, a nonzero signal can be noise, music, or a single click rather than useful speech.

This guide provides a bounded Python check for short, uncompressed 16-bit PCM WAV files. It reports each channel separately and leaves speech detection to a separate review or model. It does not modify the recording or submit it to a transcription provider.

Decide what you want to reject

Start with a narrow rule you can explain. If your application expects spoken audio, a file with no frames cannot provide it. A file whose samples are all exactly zero is a different condition: it contains a timeline, but no nonzero sample values in the inspected PCM data.

Do not silently extend that rule to quiet audio. A low-level recording may still contain speech. A recording with background noise may contain no speech despite having substantial signal energy. Treat these as candidates for listening or voice activity detection, not automatic conclusions from a peak measurement.

Keep the original artifact while investigating. If you repeatedly transcribe the same empty or zero-valued input, changing the transcription request will not restore missing source material.

Inspect channels without mixing them

Stereo channels may contain different content. If one channel is all zeros and the other is active, report that distinction before deciding whether to select, preserve, or mix the channels. Averaging channels first can hide the cause of a problem; opposite-polarity signals can cancel during a downmix.

The following helper accepts a local file path. Its scope is deliberately limited to one or two channels, two-byte PCM samples, a sample rate of at most 192,000 Hz, and at most five minutes. These are example limits for this inspection helper, not Tornado API limits or transcription requirements.

import struct
import wave


def inspect_pcm16_wav(path):
    with wave.open(str(path), "rb") as audio:
        channels = audio.getnchannels()
        rate = audio.getframerate()
        expected = audio.getnframes()
        if (audio.getcomptype() != "NONE" or audio.getsampwidth() != 2
                or channels not in (1, 2)):
            raise ValueError("requires mono or stereo 16-bit PCM WAV")
        if not 1 <= rate <= 192000 or expected > rate * 300:
            raise ValueError("outside this example's rate or duration limit")

        peaks = [0] * channels
        frames = 0
        while True:
            block = audio.readframes(4096)
            if not block:
                break
            if len(block) % (2 * channels):
                raise ValueError("incomplete PCM frame")
            frames += len(block) // (2 * channels)
            if frames > expected:
                raise ValueError("more frames than declared")
            for index, (sample,) in enumerate(struct.iter_unpack("<h", block)):
                channel = index % channels
                peaks[channel] = max(peaks[channel], abs(sample))
        if frames != expected:
            raise ValueError("truncated PCM data")

        return {
            "frames": frames,
            "duration_seconds": frames / rate,
            "peak_absolute_sample": peaks,
            "all_zero_channels": [peak == 0 for peak in peaks] if frames else None,
            "empty": frames == 0,
        }

Run inspect_pcm16_wav("audio.wav") on a completed local download. The filename is your input, not a sample hosted by Tornado. The Python wave documentation defines the header accessors and frame-reading interface; the module supports uncompressed PCM rather than arbitrary audio codecs. Unsupported or malformed inputs may raise wave.Error, EOFError, or an I/O exception before this helper returns. Keep those failures separate from an empty-audio result.

The explicit little-endian signed-short format comes from Python's struct format rules. Reading small blocks avoids loading the full sample array at once. The helper still scans every declared frame, so processing time grows with the input. It is not a complete hostile-file validator or a general audio decoder.

Interpret the result conservatively

ResultWhat it establishesNext step
empty is trueNo audio frames were readReview acquisition or export; do not label this as a silent recording
Every channel is marked all-zeroAll inspected PCM samples are exactly zeroCheck the selected source track and transformation
One channel is all-zeroThat channel has no nonzero samplesInspect the other channel before changing channel selection
All channels have nonzero peaksAt least one nonzero sample exists in each channelListen or apply a suitable speech check
An exception occursThe helper could not complete its supported inspectionDiagnose the format, file completeness, or configured limit

A single nonzero sample is enough to make a channel fail the all-zero test. That is intentional: the helper measures a precise property rather than inventing a threshold for usable speech. Its peak values are integer sample magnitudes, not loudness scores or transcription-quality estimates.

Synthetic fixtures exercise zero-valued mono audio, one active stereo channel, opposite-polarity channels, an empty WAV, truncation, unsupported sample width, and the duration limit. They validate the helper's decisions on those inputs. They do not measure speech recognition accuracy or establish compatibility with a transcription service.

Use a different check for quiet intervals

If you need to locate pauses rather than detect an entirely zero-valued channel, use a detector with an explicit amplitude threshold and minimum interval duration. FFmpeg's silence detector implementation exposes these as separate controls and supports channel-specific detection. Thresholded silence detection answers a different question from exact-zero inspection.

Choose thresholds against representative recordings, including quiet speakers and background noise. Review false positives before rejecting files automatically. Do not remove pauses merely to improve a numeric score: deleting intervals changes the relationship between transcript timestamps and the original recording. Preserve an edit map if you do make cuts; see mapping transcript timestamps to source media.

Place the check after delivery and before transcription

In a Tornado workflow, retain the ingestion job ID, verify delivery, and inspect the exact artifact that will go downstream. The public job-status reference describes status and delivery information. A completed ingestion job is not evidence that the audio contains speech, and this inspection helper is application code rather than a Tornado endpoint.

If the delivered format is outside the helper's scope, inspect it with a decoder that supports that format or create a documented analysis copy. Do not rename a compressed file to .wav. The audio output guide explains how to match the downstream service's format requirements without unnecessary conversion.

Record the input revision, channel results, and the decision to proceed or review. Avoid automatically resubmitting the source job for a content-level finding. First determine whether the silence was present in the source, introduced by track selection, or introduced by a later transformation.

Build your first Tornado media workflow with one authorized recording, then add an explicit audio-content check before expanding the transcription workload.