2024-03-01 01:14:24 -08:00
# Croaker
2024-03-05 22:51:04 -08:00
2025-09-14 15:51:42 -07:00
Croaker is a Linux desktop audio player controlled from a TCP server. It is designed specifically to play background music during TTRPG sessions.
2024-03-01 01:14:24 -08:00
2024-03-10 00:17:38 -08:00
### Features
2025-09-14 15:51:42 -07:00
* Audio playback using VLC
2024-03-10 00:17:38 -08:00
* Playlists are built using symlinks
* Randomizes playlist order the first time it is cached
* Always plays `_theme.mp3` first upon switching to a playlist, if it exists
2025-09-14 15:51:42 -07:00
* Controlled by issuing commands over a TCP socket
2024-03-10 00:17:38 -08:00
### Requirements
* A functioning shoutcast / icecast server
2025-09-14 15:51:42 -07:00
* Python >= 3.11
* python3.11-dev
2024-03-10 00:17:38 -08:00
2024-03-01 01:14:24 -08:00
## What? Why?
2025-09-14 15:51:42 -07:00
I run an online D& D game. For years I have provided my players with an internet radio station playing the session background music. The first version was built using liquidsoap and icecast. The second version replaced liquidsoap with a custom streamer implementation (which is still available on the `shoutcast` branch, warts and all).
Both of these solutions were functional but high maintenance, and I wanted something simpler both for me and my players.
This version of Croaker usees VLC (via python-vlc) to play audio locally, and pops up a read-only desktop interface to display what is playing. I share this app using screen sharing during our online games, and control it using my DM tools.
2024-03-01 01:14:24 -08:00
*Now that is a powerful yak! -- Aesop Rock (misquoted)*
2024-03-10 00:17:38 -08:00
2024-03-01 01:14:24 -08:00
## Quick Start (Server)
2024-03-10 00:25:57 -08:00
This assumes you have a functioning icecast2/whatever installation already.
2024-03-01 01:14:24 -08:00
```
% mkdir -p ~/.dnd/croaker
% croaker setup > ~/.dnd/croaker/defaults
% vi ~/.dnd/croaker/defaults # adjust to taste
% croaker add session_start /music/session_start.mp3
% croaker add battle /music/battle/*.mp3
```
Now start the server, which will begin streaming the `session_start` playlist:
2024-03-10 00:25:57 -08:00
## Controlling The Server
2024-03-01 01:14:24 -08:00
```
2024-03-05 22:51:04 -08:00
% croaker start
INFO Daemonizing controller on (localhost, 8003); pidfile and logs in ~/.dnd/croaker
2024-03-01 01:14:24 -08:00
```
2024-03-05 22:51:04 -08:00
Connnect to the command & control server:
2024-03-10 00:27:34 -08:00
```bash
2024-03-05 22:51:04 -08:00
% telnet localhost 8003
2024-03-10 00:27:34 -08:00
Trying 127.0.0.1...
Connected to croaker.local.
Escape character is '^]'.
2024-03-10 00:17:38 -08:00
help
2025-09-14 15:51:42 -07:00
PLAY PLAYLIST - Load and play the specified playlist.
LIST [PLAYLIST] - List all lplaylists or the contents of a single playlist.
BACK - Return to the previous track in the playlist
2024-03-10 00:17:38 -08:00
FFWD - Skip to the next track in the playlist.
HELP - Display command help.
KTHX - Close the current connection.
STOP - Stop the current track and stream silence.
STFU - Terminate the Croaker server.
2024-03-01 01:14:24 -08:00
```
2024-03-10 00:17:38 -08:00
List available playlists:
```
list
2024-03-01 01:14:24 -08:00
2024-03-10 00:17:38 -08:00
battle
adventure
session_start
2024-03-01 01:14:24 -08:00
```
2024-03-10 00:17:38 -08:00
Switch to battle music -- roll initiative!
```
play battle
2024-03-01 01:14:24 -08:00
OK
```
Skip this track and move on to the next:
```
2024-03-10 00:25:57 -08:00
ffwd
2024-03-01 01:14:24 -08:00
OK
```
2024-03-10 00:17:38 -08:00
Stop the music:
2024-03-01 01:14:24 -08:00
```
2024-03-10 00:25:57 -08:00
stop
2024-03-10 00:17:38 -08:00
OK
```
Disconnect:
```
kthx
KBAI
2024-03-05 22:51:04 -08:00
Connection closed by foreign host.
2024-03-01 01:14:24 -08:00
```
2024-03-10 00:25:57 -08:00
## Python Client Implementation
Here's a sample client using Ye Olde Socket Library:
```python
import socket
from dataclasses import dataclass
from functools import cached_property
@dataclass
class CroakerClient():
host: str
port: int
@cached_property
def playlists(self):
return self.send("LIST").split("\n")
def list(self, *args):
if not args:
return self.playlists
return self.send(f"LIST {args[0]}")
def play(self, *args):
if not args:
return "Error: Must specify the playlist to play."
return self.send(f"PLAY {args[0]}")
def ffwd(self, *args):
return self.send("FFWD")
def stop(self, *args):
return self.send("STOP")
def send(self, msg: str):
BUFSIZE = 4096
data = bytearray()
with socket.create_connection((self.host, self.port)) as sock:
sock.sendall(f"{msg}\n".encode())
while True:
buf = sock.recv(BUFSIZE)
data.extend(buf)
if len(buf) < BUFSIZE:
break
sock.sendall(b'KTHX\n')
return data.decode()
if __name__ == '__main__':
client = CroakerClient(host='localhost', port=1234)
client.play('session_start')
```