Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PulseStream

Stream Windows audio to a Linux machine over TCP using ALSA. Built with Rust and iced.

Windows Rust CI

Features

  • WASAPI loopback capture — captures system audio with low latency using Windows Audio Session API
  • VB-CABLE capture mode — optional alternative that captures from VB-CABLE virtual audio device; auto-detected when installed, no local speaker output and no mute workaround needed
  • Per-app audio capture — isolate and stream audio from a single application via process loopback
  • Auto server discovery — scans the local subnet to find receivers
  • Real-time stats — displays bandwidth, latency, capture format, and uptime
  • System volume integration — reads Windows volume/mute state and applies it to the stream
  • Start with Windows — launches at boot minimized to tray, enabled by default
  • Mute local output — silences laptop speakers while streaming; audio only plays on the receiver
  • System tray — starts hidden in tray; restore with a click, exit from the context menu
  • Dark / Light theme — cyan-accented theme with a toggle in the header
  • Persistent settings — server, port, audio format, and preferences saved to settings.json
  • Auto-reconnect — retries the connection automatically on network failure

Download

Grab the latest .exe from the Releases page — no installation required.

Building from source

Prerequisites

  • Rust (stable, 2021 edition)
  • Windows 10 or later

Build

cargo build --release

The compiled binary will be at target/release/pulse-stream.exe.

Run

cargo run

Test

cargo test

193 tests cover input validation, settings serialization, audio streamer lifecycle, theme properties, capture mode, and app state transitions.

Usage

Receiver setup (Linux)

The receiver script listens on a TCP port and pipes audio straight to ALSA with a small buffer for low-latency playback:

Port note: PulseAudio's network protocol uses 47134714 by default. If your receiver host also runs PulseAudio (e.g. a multi-purpose audio LXC), pick a non-conflicting port like 4715. Otherwise PulseAudio will grab the port first and silently consume your stream as protocol garbage.

#!/bin/bash
# pulse-stream-recv.sh — low-latency TCP-to-ALSA receiver
PORT=${1:-4715}
RATE=48000
CHANNELS=2
FORMAT=S16_LE
# ALSA buffer/period are measured in FRAMES (not bytes).
# 256-frame period × 2 periods = 512-frame buffer = ~10.7ms at 48kHz.
PERIOD_FRAMES=256
PERIODS=2
BUFFER_FRAMES=$((PERIOD_FRAMES * PERIODS))

while true; do
  # Kill any stale ncat still holding the port from a previous cycle
  pkill -f "ncat -l -p $PORT" 2>/dev/null
  sleep 0.3

  ncat -l -p "$PORT" | aplay \
    -t raw \
    -f "$FORMAT" \
    -r "$RATE" \
    -c "$CHANNELS" \
    --buffer-size=$BUFFER_FRAMES \
    --period-size=$PERIOD_FRAMES \
    -D plughw:0,0 \
    2>/dev/null

  sleep 0.5
done

Make it executable and run:

chmod +x pulse-stream-recv.sh
./pulse-stream-recv.sh 4715

Requires ncat (from nmap) and alsa-utils. Install with: sudo apt install ncat alsa-utils (Debian/Ubuntu) or sudo pacman -S nmap alsa-utils (Arch).

Persisting across reboots (systemd)

To run the ALSA receiver as a service that starts on boot and auto-reconnects:

  1. Copy the script above to /usr/local/bin/pulse-stream-recv.sh and make it executable:
sudo chmod +x /usr/local/bin/pulse-stream-recv.sh
  1. Create the service file:
sudo tee /etc/systemd/system/pulse-stream-recv.service > /dev/null << 'EOF'
[Unit]
Description=PulseStream low-latency ALSA receiver
After=network.target sound.target
Wants=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/pulse-stream-recv.sh 4715
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF
  1. Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable pulse-stream-recv.service
sudo systemctl start pulse-stream-recv.service

Check status with systemctl status pulse-stream-recv.service. The service automatically restarts when a stream disconnects and is ready for the next connection.

The receiver script handles stale connections internally — it kills any leftover ncat process holding the port before each listen cycle. Combined with systemd's Restart=always, no external watchdog is needed.

Troubleshooting

Receiver logs Ncat: bind to [::]:<port>: Address already in use. QUITTING.

Something else owns the port. Check with ss -tlnp | grep <port>. The most common offender on shared audio hosts is PulseAudio (pulseaudio process holding 4713/4714). Either:

  • Move the receiver to a free port (e.g. 4715) by editing ExecStart= in the unit file and running systemctl daemon-reload && systemctl restart pulse-stream-recv.service, then update the port in the Windows app, or
  • Stop PulseAudio if it's not needed: systemctl stop pulseaudio.service && systemctl disable pulseaudio.service

Connection succeeds but no audio plays

ncat is forwarding bytes to the wrong ALSA device, or another process has it. Run aplay -l to list devices and update -D default in the script to the correct hardware (e.g. -D plughw:0,0). Check fuser -v /dev/snd/* for processes holding the device.

Sender setup (Windows)

  1. Launch PulseStream on Windows
  2. Enter the server IP and port, or click the scan button to auto-detect
  3. Select an audio device and optionally a specific application
  4. Click Connect

VB-CABLE mode (optional)

By default, PulseStream captures audio via WASAPI loopback on the default speaker and mutes it so sound only plays on the receiver. VB-CABLE provides a cleaner alternative — audio never reaches physical speakers at all.

How it works:

WASAPI Loopback:   Apps → Speaker → Loopback Capture → PulseStream
VB-CABLE:          Apps → VB-CABLE Output → VB-CABLE Input → PulseStream

Setup:

  1. Download and install VB-CABLE (free)
  2. Restart PulseStream — a Mode toggle appears in the Audio Source section
  3. Select VB-CABLE — PulseStream automatically:
    • Switches your Windows default output to VB-CABLE
    • Captures from VB-CABLE's input side
    • Routes system volume keys to control the stream level
  4. When you disconnect or switch back, the original output device is restored

VB-CABLE is freeware and cannot be bundled. PulseStream only detects it — install it yourself from vb-audio.com/Cable.

Configuration

Settings are stored at:

%LOCALAPPDATA%\PulseStream\data\settings.json
Setting Default Description
server (empty — triggers auto-scan) Receiver IP
port 4714 TCP port (use 4715 if PulseAudio runs on the receiver)
rate 48000 Sample rate in Hz
channels 2 Channel count (1–8)
device_id null Audio output device (null = default)
auto_connect false Connect on startup
start_with_windows true Register in Windows startup
minimize_to_tray true Start hidden in tray; minimize on close
mute_local_output false Mute laptop speakers while streaming
capture_mode "loopback" Capture mode: "loopback" or "vbcable"
dark_theme true Dark mode enabled

Key design decisions:

  • Audio capture runs on a dedicated thread communicating via flume channels to keep the UI responsive
  • Pre-allocated reusable buffers in the capture loop eliminate per-frame heap allocations
  • TCP_NODELAY with a small send buffer minimizes streaming latency
  • COM is initialized per-thread to avoid apartment model conflicts with the iced/winit event loop

Problem & Motivation

Streaming audio from a Windows PC to a Linux machine typically requires third-party tools that add significant overhead. Existing solutions often suffer from:

  • High latency — multiple layers of buffering between capture, encoding, network, and playback
  • No per-app isolation — you stream everything or nothing, with no way to pick a single application
  • Heavy dependencies — requiring virtual audio drivers or complex audio server configurations on Windows

PulseStream solves this by using WASAPI loopback capture to read audio directly from the Windows audio engine and streaming raw PCM over a simple TCP socket to an ALSA receiver on Linux. No encoding, minimal dependencies on both ends. An optional VB-CABLE mode provides silent local output without the mute workaround.

Latency

The end-to-end audio pipeline has several stages, each contributing delay:

Stage Typical delay Notes
WASAPI capture buffer ~10 ms Set to 10 ms (100,000 × 100 ns units)
PCM conversion < 0.1 ms Zero-copy i16 conversion via direct memory write
TCP send 0.1–2 ms TCP_NODELAY enabled, 1920-byte send buffer
Network transit 0.1–1 ms Wired LAN recommended
ALSA receiver buffer ~10 ms 256 frames × 2 periods at 48 kHz

Where the delay comes from

The sender side (WASAPI capture → TCP send) adds ~12 ms total. The ALSA receiver buffer adds ~10 ms (256 frames × 2 periods at 48 kHz), for ~25 ms total — low enough that delay is not perceptible for most use cases.

Additional tips

  • Use a wired Ethernet connection — WiFi adds jitter and occasional 5–20 ms spikes
  • Output directly to hardware with -D plughw:X,Y (find the index via aplay -l) rather than -D default. On hosts running PulseAudio/PipeWire, default routes through the sound server and adds an extra buffering layer (often 20–40 ms)
  • Remember that aplay's --buffer-size/--period-size are in frames, not bytes — a 256-frame period at 48 kHz is ~5.3 ms. Multiplying by channels/bytes-per-sample silently inflates the buffer (e.g. 4× for S16 stereo) and is a common cause of unexpected delay

Limitations

  • Windows only — WASAPI is a Windows API; the sender must run on Windows 10 or later
  • No audio encoding — streams raw PCM (s16le), so bandwidth usage is proportional to sample rate and channel count (~1.5 Mbps at 48 kHz stereo). Not suitable over the internet or slow networks
  • No encryption — audio is sent as plaintext TCP. Use only on trusted local networks
  • Receiver must run ALSA — uses the provided ALSA receiver script with ncat and aplay. No native PipeWire or macOS/Windows receiver support
  • Single receiver — streams to one TCP endpoint at a time; no multicast or multi-client support
  • No sample rate conversion — the sender captures at the device's native rate and sends as-is. The rate and channels fields must match the receiver's aplay configuration
  • Per-app capture requires Windows 10 2004+ — process loopback (AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK) is only available on Windows 10 version 2004 and later
  • System tray requires a window manager — the tray icon uses OS-level system tray APIs; headless or terminal-only Windows environments are not supported

License

MIT

About

Stream Windows audio to Linux

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages