jacadi is a simple Go HTTP API server that plays WAV audio files through a USB speaker. It is designed to run on a Raspberry Pi or similar with a USB speaker attached, and allow clients to play sounds via HTTP requests. Supports both single-file playback and folder mode for looping ambient audio.
jacadi's main ambition is to serve as a voice proxy to command voice controlled devices (like alexa, ok google, etc).
On your Raspberry Pi, check that the USB speaker is detected:
aplay -lYou should see your USB speaker listed. Note the card number.
# Build the Docker image (slim - pre-generated audio only)
docker build --target slim -t jacadi:slim .
# Or build full image with TTS support
docker build --target full -t jacadi:full .
# Build with a specific routeset (defaults to dreame)
docker build --target slim --build-arg ROUTES=mydevice -t jacadi:mydevice .
# Start the service
docker-compose up -d# Health check
curl http://localhost:8080/health
# Play audio (format: /play/{device}/{command})
curl -X POST http://localhost:8080/play/dreame/ok-dream
curl -X POST http://localhost:8080/play/dreame/clean-kitchen
# Start folder (loops infinitely)
curl -X POST http://localhost:8080/play/dreame/ambient
# Stop folder
curl -X POST http://localhost:8080/stop
# Set volume (0-100)
curl -X POST http://localhost:8080/volume \
-H "Content-Type: application/json" \
-d '{"volume": 50}'
# Get current volume
curl http://localhost:8080/volumeThe full image embeds piper, allowing on the fly TTS through the API:
curl -X POST http://localhost:8080/play/tts \
-H "Content-Type: application/json" \
-d '{"text": "Hello world"}'
curl -X POST http://localhost:8080/play/tts \
-H "Content-Type: application/json" \
-d '{"text": "Hello world", "voice": "en_US-amy-low"}'EXTRA_ROUTES_PATH: Path to extra routes file for runtime merging (optional)EXTRA_ROUTES_JSON: Inline JSON string of extra routes for runtime merging (optional, merged afterEXTRA_ROUTES_PATH)HOST: Listen address (default:0.0.0.0)PORT: Listen port (default:8080)AUDIODEV: ALSA device for audio output (e.g.,hw:3,0)ALSA_CONTROL: ALSA mixer control name for volume (default:PCM, useMasterfor internal sound cards){DEVICE}_VOLUME_OVERRIDE: Force volume for a specific device, ignoring the route config value (e.g.,DREAME_VOLUME_OVERRIDE=20). Device name is uppercased.VOICE: Default piper voice model (default:en_US-amy-low)
Routes are stored in routes/{name}.json. Use the ROUTES build arg to select a routeset (defaults to dreame).
{
"dreame": {
"volume": 80,
"commands": {
"ok-dream": { "text": "Okay dream" },
"clean-kitchen": { "text": "Clean the kitchen" },
"ambient": { "text": "Ambient music", "type": "folder" },
"music": { "text": "Background music", "type": "folder", "path": "/music/library" },
"jingle": { "text": "Jingle loop", "type": "folder", "restart": true }
}
}
}- Top-level keys are device names (creates
/play/{device}/...endpoints) volume: Optional device volume (0-100). When set, playback saves current volume, sets device volume, plays audio, then restores original volume. For folders, volume is set but not restored (folder runs indefinitely).commands: Map of command names to metadatatext: Description of the commandtype: Playback type (omit for single file,"folder"for directory loop)path: Optional custom path for folder directory (overrides default location)restart: Optional boolean for folder commands. Whentrue, always starts from the beginning instead of resuming from last position
- Audio locations:
- Single file:
assets/audio/{device}/{command}.wav(copied to/audio/at build time) - Folder:
assets/audio/{device}/{command}/directory containing audio files (or custompath)
- Single file:
Add routes at runtime without rebuilding. Two options:
File mount (EXTRA_ROUTES_PATH) — useful for Docker Compose:
volumes:
- "./extra_routes.json:/app/extra_routes.json:ro"
- "./extra_audio:/audio/extra"
environment:
- EXTRA_ROUTES_PATH=/app/extra_routes.jsonInline JSON (EXTRA_ROUTES_JSON) — useful for Kubernetes ConfigMaps/Secrets (no volume mount required):
environment:
- EXTRA_ROUTES_JSON={"mydevice":{"commands":{"hello":{"text":"Hello"}}}}Both can be used simultaneously; EXTRA_ROUTES_PATH is merged first, then EXTRA_ROUTES_JSON. Same device/command keys in EXTRA_ROUTES_JSON override those from EXTRA_ROUTES_PATH. Invalid JSON in EXTRA_ROUTES_JSON logs a warning and is skipped.
Audio files go in /audio/extra/{device}/{command}.wav. For folders, create a directory /audio/extra/{device}/{command}/ containing audio files.
- Full image: Missing audio files are auto-generated at startup using piper TTS (works for both
EXTRA_ROUTES_PATHandEXTRA_ROUTES_JSON) - Slim image: You must provide the audio files manually
In docker, ensure /audio/extra is a volume so the generated audio files persist.
Audio files must be WAV format: 44100 Hz, 16-bit, mono.
Convert with ffmpeg:
ffmpeg -i input.wav -ar 44100 -ac 1 -acodec pcm_s16le output.wavGenerate rest_command configuration for Home Assistant from your routes:
go run cmd/generate-homeassistant/main.go -base-url="http://jacadi.local:8080"With TTS support (full image only):
go run cmd/generate-homeassistant/main.go -base-url="http://jacadi.local:8080" -ttsOutput goes to ha-config/. Include in your Home Assistant configuration.yaml:
rest_command: !include homeassistant_rest.yml
script: !include homeassistant_scripts.ymlAn Ansible role is available at ansible-role-jacadi for deploying the container.