A network server that provides HTTP access to FreeBSD's /dev/speaker
device for remote melody playback.
SPKRD exposes FreeBSD's built-in speaker device over HTTP, allowing you to play melodies remotely from any system that can make HTTP requests. The server handles device concurrency automatically with configurable retry logic.
- HTTP API - Simple PUT endpoint for melody playback
- Device Retry Logic - Automatically retries when device is busy (1s intervals, configurable timeout)
- Input Validation - Melody length limits and UTF-8 validation
- Configurable Device Path - Use custom device paths for testing or alternative devices
- Daemon Support - Run as background daemon with PID file management
- Flexible Logging - Syslog for daemon mode, stderr for foreground, with debug logging support
- Request Logging - Timestamps, client IPs, and printable melody content (debug mode only)
- Example Clients - Ready-to-use clients in Rust and Go
The FreeBSD speaker device (/dev/speaker
) accepts melody strings in a specific format. For complete documentation of the melody syntax, see the FreeBSD manual:
FreeBSD speaker(4) Manual Page
- Notes:
a
,b
,c
,d
,e
,f
,g
(with optional#
or+
for sharp) - Octaves:
o1
too7
(default o4) - Length:
l1
,l2
,l4
,l8
,l16
,l32
(whole, half, quarter, etc.) - Tempo:
t60
tot255
(beats per minute) - Pause:
p
followed by length - Repeat:
.
after note extends by half
Example: "t120l4 c d e f g a b o5c"
- Rust 1.70+ (for server)
- Go 1.19+ (for Go client example)
- FreeBSD system with
/dev/speaker
device
# Clone and build the server
git clone <repository-url>
cd spkrd
cargo build --release
# Build example clients
cd examples
cargo build --release # Rust client
go build client.go # Go client
For production deployment as a system service on FreeBSD:
# Install to system directories (default: /usr/local)
make install
# Or install to custom location
make install DSTDIR=/usr/local
# Or install with custom program name
make install PROGRAM=my-spkrd
This installs:
- Binary to
/usr/local/bin/spkrd
- FreeBSD rc.d script to
/usr/local/etc/rc.d/spkrd
Add the following to /etc/rc.conf
to enable the service:
# Enable the service
spkrd_enable="YES"
# Configure server options via flags
spkrd_flags="--port 1111 --device /dev/speaker --retry-timeout 30"
Available configuration flags:
--port <port>
- Server port (default: 1111)--device <path>
- Speaker device path (default: /dev/speaker)--retry-timeout <secs>
- Device retry timeout (default: 30)--daemon
- Run as background daemon (automatically added by rc.d)--pidfile <path>
- PID file path (default: /var/run/spkrd.pid)--debug/-d
- Enable debug logging including client request details
Example configurations:
# Custom port
spkrd_flags="--port 3000"
# Different device and port
spkrd_flags="--device /tmp/test-speaker --port 9000"
# Extended timeout
spkrd_flags="--retry-timeout 60 --port 1111"
# Enable debug logging (shows client requests in logs)
spkrd_flags="--debug --port 1111"
# Custom PID file location for non-root execution
spkrd_flags="--pidfile /tmp/spkrd.pid --port 1111"
# Start the service
service spkrd start
# Stop the service
service spkrd stop
# Restart the service
service spkrd restart
# Check service status
service spkrd status
SPKRD supports flexible logging with different outputs depending on execution mode:
- Uses syslog with facility
daemon
- Logs go to system log (typically
/var/log/daemon.log
or/var/log/messages
) - View logs:
tail -f /var/log/daemon.log | grep spkrd
- Uses stderr with timestamps
- Logs appear directly in terminal
- Suitable for development and manual testing
- Default: Startup messages (with all configuration) and errors only
- Debug (
--debug/-d
): Adds client request logging including:- Client IP address
- Printable characters from melody data
- Request status and retry count
- Completion status
# View daemon logs on FreeBSD
tail -f /var/log/daemon.log | grep spkrd
# Run with debug logging in foreground
./spkrd --debug --port 1111
# Service with debug logging (via rc.conf)
spkrd_flags="--debug"
service spkrd restart
Sample log output:
# Startup (always logged)
Jan 29 10:30:15 hostname spkrd[1234]: Starting spkrd server: port=1111, retry_timeout=30s, device=/dev/speaker, daemon=true, pidfile=/var/run/spkrd.pid, debug=false
# Error (always logged)
Jan 29 10:30:16 hostname spkrd[1234]: Device error for request from 192.168.1.100: Permission denied
# Debug request logging (--debug only)
Jan 29 10:30:17 hostname spkrd[1234]: Request from 192.168.1.100: melody=t120l4cdefgab
Jan 29 10:30:17 hostname spkrd[1234]: Request from 192.168.1.100 completed successfully after 0 retries
# Basic usage (default port 1111, device /dev/speaker)
./target/release/spkrd
# Custom configuration
./target/release/spkrd --port 3000 --retry-timeout 60 --device /dev/speaker
# For testing with a regular file
./target/release/spkrd --device /tmp/test-speaker
# Run as daemon
./target/release/spkrd --daemon
# Run with debug logging
./target/release/spkrd --debug
--port
- Server port (default: 1111)--retry-timeout
- Device retry timeout in seconds (default: 30)--device
- Path to speaker device (default: /dev/speaker)--daemon
- Run as background daemon--pidfile
- Path to PID file (default: /var/run/spkrd.pid)--debug/-d
- Enable debug logging including client request details
curl -X PUT http://localhost:1111/play -d "cdefgab"
- 200 - Melody played successfully (empty body)
- 400 - Invalid melody (error message in body)
- 503 - Device busy/timeout (error message in body)
- 500 - Server error (error message in body)
The examples/
directory contains ready-to-use client implementations in Rust and Go.
Quick Examples:
# Rust client with config file
cd examples
echo "http://server:1111" > ~/.spkrc
./target/release/client "cdefgab"
# Go client
go run client.go http://server:1111 "cdefgab"
For complete client documentation, build instructions, and usage examples, see examples/README.md.
import requests
response = requests.put('http://server:1111/play', data='cdefgab')
if response.status_code == 200:
print("Melody played successfully")
else:
print(f"Error: {response.text}")
# Run all tests (uses temporary files as mock devices)
cargo test
# Run with verbose output
cargo test -- --nocapture
spkrd/
├── src/
│ ├── main.rs # CLI entry point
│ ├── lib.rs # Library interface
│ ├── server.rs # HTTP server
│ ├── speaker.rs # Device handling
│ └── error.rs # Error types
├── tests/
│ └── integration_tests.rs # Integration tests
├── examples/
│ ├── client.rs # Rust client
│ ├── client.go # Go client
│ └── Cargo.toml # Client dependencies
├── API.md # Detailed API documentation
└── README.md # This file
- HTTP Request - Client sends PUT request to
/play
with melody data - Validation - Server validates melody length (≤1000 chars) and UTF-8 encoding
- Device Access - Server attempts to open the speaker device
- Retry Logic - If device is busy (EBUSY), retry every 1 second until timeout
- Playback - Write melody to device and close
- Response - Return appropriate HTTP status code
If you get permission errors accessing /dev/speaker
:
# Check device permissions
ls -l /dev/speaker
# Add user to appropriate group (typically 'wheel' or 'operator')
sudo pw groupmod wheel -m username
# Or run with sudo (not recommended for production)
sudo ./target/release/spkrd
The server automatically retries when the device is busy. If you consistently get timeout errors:
- Increase
--retry-timeout
value - Check if another process is using the speaker device
- Verify the device path is correct
Use a regular file as a mock device for testing:
# Start server with file device
./target/release/spkrd --device /tmp/test-speaker
# Send melody
curl -X PUT http://localhost:1111/play -d "cdefgab"
# Check result
cat /tmp/test-speaker
This project is licensed under the BSD 2-Clause License. See the LICENSE file for details.
Copyright (c) 2025, Raphael Poss
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass with
cargo test
- Submit a pull request