← All Problems

12. Sliding-Window Silence Detection in a Streamed Audio Signal

Confirmed Medium Sliding Window / Signal Processing
Grounding: Confirmed: Cartesia's Ink-2 STT model performs "semantic endpointing" — turn detection driven by meaning rather than pure silence/VAD — and emits turn.start / turn.eager_end / turn.end events (cartesia.ai/blog/ink-2). This problem implements the classic energy-based silence/VAD primitive such a pipeline would still use as one low-level signal feeding into (but not replacing) that semantic layer; Cartesia has not published the internals of that layer, so treat this as the general building block, not a description of Ink-2's actual implementation.

Problem

Before any semantic turn-taking logic runs, a streaming voice pipeline needs a cheap low-level signal for "is anyone talking right now." A classic building block for that is windowed RMS energy: split the raw audio stream into fixed-size frames and flag any frame whose energy falls below a threshold as silence.

Given a flat list of audio samples, split them into consecutive, non-overlapping frames of window_size samples each (the final frame may be shorter if the signal doesn't divide evenly), and return one boolean per frame: True if that frame's RMS energy is below energy_threshold.

Source: src/12_silence_window_detector.py

def detect_silence_windows(samples: list[float], window_size: int, energy_threshold: float) -> list[bool]:

>>> detect_silence_windows([0, 0, 0, 0, 0.9, -0.9, 0.8, -0.8, 0.01, -0.01, 0.02, -0.02], window_size=4, energy_threshold=0.1)
[True, False, True]

>>> detect_silence_windows([1.0, 1.0], window_size=4, energy_threshold=0.1)
[False]

Step-by-Step Approach

  1. Iterate over the samples in steps of window_size using range(0, len(samples), window_size), which naturally produces the start index of each consecutive, non-overlapping frame.
  2. Slice out each frame with samples[start:start + window_size] — Python slicing handles the shorter final frame automatically without any bounds-checking code.
  3. Compute RMS (root-mean-square) energy for the frame: square every sample, average the squares, then take the square root — math.sqrt(sum(x*x for x in frame) / len(frame)).
  4. RMS is used instead of a simple average because audio samples oscillate around zero; squaring first makes negative and positive swings both contribute to energy instead of canceling out.
  5. Append rms < energy_threshold to the result list for each frame, and return the full list once every frame has been processed.

The key insight is that windowed RMS energy is a purely local, O(window_size) computation per frame with no dependency between frames, so the whole pass is O(n) with O(1) extra memory beyond the output — there's no need for a running/incremental energy trick unless the windows overlap, which they don't here.

Reference solution

import math


def detect_silence_windows(samples: list[float], window_size: int, energy_threshold: float) -> list[bool]:
    # non-overlapping frames: O(n) total work, one RMS computation per frame
    result = []
    for start in range(0, len(samples), window_size):
        frame = samples[start:start + window_size]
        # RMS = sqrt(mean(x^2)); classic energy measure, robust to sign
        rms = math.sqrt(sum(x * x for x in frame) / len(frame))
        result.append(rms < energy_threshold)
    return result

Key Functions & Tricks

  • range(0, len(samples), window_size) — yields the start index of each consecutive frame in one line.
  • samples[start:start + window_size] — slices out a frame; Python silently truncates at the end, handling the short final frame for free.
  • math.sqrt(sum(x*x ...) / len(frame)) — the RMS formula — squares samples so positive/negative swings both add to energy.
  • list comprehension inside sum() — computes sum of squares in one expression without a manual accumulator loop.

How to Recognize This Pattern

Reach for windowed/framed processing whenever a problem describes a continuous signal that needs to be evaluated in fixed-size chunks — audio, sensor readings, or any time series where "per-frame" or "per-window" summary statistics are requested. The tell here is "non-overlapping" windows, which makes this simpler than sliding-window problems that ask for a rolling statistic per position (those need incremental updates to stay O(n); see the sliding-window-maximum family of problems). A common variation asks for overlapping windows with a stride smaller than the window size, which does require an incremental running-sum trick to stay efficient. A common pitfall is using mean absolute value or peak amplitude instead of RMS — RMS is the standard energy measure because it weights louder transients more heavily (via the square) than a plain average would.