Pipe Through FFmpeg
Overview
Section titled “Overview”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.
How It Works
Section titled “How It Works”The data flow is straightforward but involves careful concurrency:
- SDL2 capture callback grabs PCM samples from the input device and writes them to FFmpeg’s stdin.
- FFmpeg reads raw
s16leaudio from stdin, applies the configured filter chain, and writes processed samples to stdout. - A reader thread pulls from FFmpeg’s stdout and pushes samples into a lock-free-style ring buffer.
- 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 stdinimpl 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); } }}Features
Section titled “Features”- 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.tomlorPiper.jsonfile 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/cliandvcpkgfor SDL2 dependency management
Configuration
Section titled “Configuration”The tool reads from Piper.toml by default (or pass --config=path):
ffmpeg_path = "/opt/homebrew/bin/ffmpeg"buffer_size = 10240filters = [ "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.
Tech Stack
Section titled “Tech Stack”| Language | Rust |
| Audio I/O | SDL2 (via sdl2 crate with static linking) |
| Processing | FFmpeg (spawned as subprocess) |
| Node bindings | NAPI-RS (napi, napi-derive) |
| Config | toml + serde_json for TOML/JSON parsing |
| Build deps | vcpkg (SDL2 + SDL2-mixer), napi-build |
| CI | GitHub Actions (macOS ARM64, Windows x64) |
| Package manager | Yarn 4 (Berry) |
| Test runner | AVA |
Source Code
Section titled “Source Code”The source code is available on the project’s GitHub repository.