Skip to content

Pipe Through FFmpeg

Pipe Through FFmpeg is a native command-line tool that captures audio from any input device, pipes it through an FFmpeg filter chain in real time, and plays the processed result out of a chosen output device. Built in Rust with N-API bindings, it ships as a single npx-installable package with prebuilt binaries for macOS (ARM64) and Windows (x64) — no Rust toolchain required for end users.

The primary use case is live audio processing: applying noise reduction, volume adjustments, or any combination of FFmpeg’s hundreds of audio filters to a microphone feed before it reaches speakers or a virtual audio device.

The data flow is straightforward but involves careful concurrency:

  1. SDL2 capture callback grabs PCM samples from the input device and writes them to FFmpeg’s stdin.
  2. FFmpeg reads raw s16le audio from stdin, applies the configured filter chain, and writes processed samples to stdout.
  3. A reader thread pulls from FFmpeg’s stdout and pushes samples into a lock-free-style ring buffer.
  4. SDL2 playback callback drains the ring buffer into the output device.

All inter-thread communication uses Arc<Mutex<...>> around the ring buffer and FFmpeg’s stdio handles. The ring buffer silently fills remaining samples with silence when underruns occur, avoiding clicks.

// The Piper struct implements SDL2's AudioCallback — on every capture frame,
// raw samples are piped directly into FFmpeg's stdin
impl AudioCallback for Piper {
type Channel = i16;
fn callback(&mut self, samples: &mut [i16]) {
if let Err(e) = self.update_input(samples) {
eprintln!("Error Piping Input: {}", e.message);
}
}
}
  • Real-time audio piping — Captures from any SDL2-supported input device, processes through FFmpeg, and plays back on any output device with minimal latency
  • Arbitrary FFmpeg filter chains — Stack any combination of FFmpeg audio filters (noise reduction via arnndn, volume control, EQ, etc.)
  • Device selection by name — Configure input/output devices by name (e.g. "BlackHole 2ch") or by index, with interactive fallback prompts
  • TOML & JSON configuration — Define your setup in a Piper.toml or Piper.json file with input/output devices, FFmpeg path, filters, and buffer size
  • Custom ring buffer — Purpose-built circular buffer balances latency against dropout prevention with configurable capacity
  • Cross-platform native binaries — Prebuilt for macOS ARM64 and Windows x64 via NAPI-RS, distributed as npm optional dependencies
  • File input mode — Can also pipe a file through the filter chain instead of a live device, useful for batch processing or testing
  • CI/CD with GitHub Actions — Automated cross-platform builds using napi-rs/cli and vcpkg for SDL2 dependency management

The tool reads from Piper.toml by default (or pass --config=path):

ffmpeg_path = "/opt/homebrew/bin/ffmpeg"
buffer_size = 10240
filters = [
"arnndn=m=bd.rnnn",
"volume=1.0"
]
input_device_name = "BlackHole 2ch"
output_device_name = "MacBook Pro Speakers"

The filters array maps directly to FFmpeg’s -filter:a argument — any valid FFmpeg audio filter works. The bundled bd.rnnn model file enables RNNoise-based noise suppression out of the box.

LanguageRust
Audio I/OSDL2 (via sdl2 crate with static linking)
ProcessingFFmpeg (spawned as subprocess)
Node bindingsNAPI-RS (napi, napi-derive)
Configtoml + serde_json for TOML/JSON parsing
Build depsvcpkg (SDL2 + SDL2-mixer), napi-build
CIGitHub Actions (macOS ARM64, Windows x64)
Package managerYarn 4 (Berry)
Test runnerAVA

The source code is available on the project’s GitHub repository.