Stream Windows audio to a Linux machine over TCP using ALSA. Built with Rust and iced.
- 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
Grab the latest .exe from the Releases page — no installation required.
- Rust (stable, 2021 edition)
- Windows 10 or later
cargo build --releaseThe compiled binary will be at target/release/pulse-stream.exe.
cargo runcargo test193 tests cover input validation, settings serialization, audio streamer lifecycle, theme properties, capture mode, and app state transitions.
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
4713–4714by default. If your receiver host also runs PulseAudio (e.g. a multi-purpose audio LXC), pick a non-conflicting port like4715. 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
doneMake it executable and run:
chmod +x pulse-stream-recv.sh
./pulse-stream-recv.sh 4715Requires
ncat(from nmap) andalsa-utils. Install with:sudo apt install ncat alsa-utils(Debian/Ubuntu) orsudo pacman -S nmap alsa-utils(Arch).
To run the ALSA receiver as a service that starts on boot and auto-reconnects:
- Copy the script above to
/usr/local/bin/pulse-stream-recv.shand make it executable:
sudo chmod +x /usr/local/bin/pulse-stream-recv.sh- 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- Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable pulse-stream-recv.service
sudo systemctl start pulse-stream-recv.serviceCheck 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.
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 editingExecStart=in the unit file and runningsystemctl 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.
- Launch PulseStream on Windows
- Enter the server IP and port, or click the scan button to auto-detect
- Select an audio device and optionally a specific application
- Click Connect
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:
- Download and install VB-CABLE (free)
- Restart PulseStream — a Mode toggle appears in the Audio Source section
- 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
- 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.
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
flumechannels to keep the UI responsive - Pre-allocated reusable buffers in the capture loop eliminate per-frame heap allocations
TCP_NODELAYwith a small send buffer minimizes streaming latency- COM is initialized per-thread to avoid apartment model conflicts with the iced/winit event loop
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.
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 |
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.
- 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 viaaplay -l) rather than-D default. On hosts running PulseAudio/PipeWire,defaultroutes through the sound server and adds an extra buffering layer (often 20–40 ms) - Remember that
aplay's--buffer-size/--period-sizeare 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
- 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
ncatandaplay. 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
rateandchannelsfields must match the receiver'saplayconfiguration - 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
MIT