Skip to content

Piper

Piper is a real-time audio processing tool that captures audio from any system input device, pipes it through configurable FFmpeg filter chains, and plays the result back on a chosen output device. The core engine is written in Rust and exposed to Node.js as a native addon via NAPI-RS, combining Rust’s low-level performance with a simple JavaScript API.

The primary use case is live noise reduction — the bundled RNNoise deep-learning model removes background noise in real time — but the architecture supports any FFmpeg audio filter chain, making it a flexible building block for audio processing workflows.

The data flow is straightforward but involves careful multi-threaded coordination:

  1. SDL2 captures raw audio from the selected input device (48 kHz, 16-bit stereo)
  2. The Rust AudioCallback converts samples to bytes and writes them to FFmpeg’s stdin
  3. FFmpeg applies the configured filter chain (e.g. arnndn noise reduction) and writes processed audio to stdout
  4. A dedicated reader thread parses FFmpeg’s stdout back into samples and pushes them into a custom ring buffer
  5. SDL2’s playback callback reads from the ring buffer and sends processed audio to the output device
┌──────────┐ raw PCM ┌────────┐ filtered PCM ┌─────────────┐
│ Mic/Input├───────────────►│ FFmpeg ├──────────────────►│ Ring Buffer │
│ (SDL2) │ via stdin │ arnndn │ via stdout │ (Rust) │
└──────────┘ └────────┘ └──────┬──────┘
┌──────▼──────┐
│ Speakers │
│ (SDL2) │
└─────────────┘
  • Native Rust core — Performance-critical audio I/O and buffering implemented in Rust, compiled to a native .node addon with NAPI-RS and LTO-optimized release builds
  • Live noise reduction — Ships with an RNNoise model (bd.rnnn) for ML-based denoising via FFmpeg’s arnndn filter
  • Pluggable FFmpeg filters — Configure any FFmpeg audio filter chain in Piper.toml; the engine builds the FFmpeg command dynamically from the filters array
  • Custom ring buffer — A lock-guarded circular buffer (RingBuffer in lib.rs) with configurable capacity to balance latency against dropout risk
  • Cross-platform device routing — SDL2 enumerates system audio devices; select input/output by name or index, or get an interactive prompt at startup
  • TOML + JSON config — Configuration loaded via serde with support for both formats, platform-specific FFmpeg paths, and sensible defaults
  • npx-runnable — Install and run in one step with npx pipe-through-ffmpeg

All settings live in a Piper.toml file at the project root:

# Path to FFmpeg binary (auto-detected if on PATH)
# ffmpeg_path = "node_modules/ffmpeg-helper/win32-x64.exe"
# Ring buffer capacity — higher = more latency, fewer dropouts
buffer_size = 10240
# FFmpeg audio filters to apply
filters = ["arnndn=m=bd.rnnn"]
# Device selection (omit to get an interactive prompt)
# input_device_name = "BlackHole 2ch"
# output_device_name = "MacBook Pro Speakers"

The Rust engine assembles this into an FFmpeg command like:

-hide_banner -f s16le -ar 48000 -ac 2 -i pipe:0 -filter:a arnndn=m=bd.rnnn -f s16le -ac 2 -ar 48000 -
Core LanguageRust
Node.js BindingNAPI-RS
Audio I/OSDL2 (via sdl2 crate)
Audio ProcessingFFmpeg (subprocess, piped stdin/stdout)
Serializationserde + toml + serde_json
FrontendNode.js / JavaScript (thin wrapper)
PlatformsmacOS (ARM64), Windows (x64)

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