# Croaker Croaker is a Linux desktop audio player controlled from a TCP server. It is designed specifically to play background music during TTRPG sessions. ### Features * Audio playback using VLC * 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 * Controlled by issuing commands over a TCP socket ### Requirements * A functioning shoutcast / icecast server * Python >= 3.11 * python3.11-dev ## What? Why? 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. *Now that is a powerful yak! -- Aesop Rock (misquoted)* ## Quick Start (Server) This assumes you have a functioning icecast2/whatever installation already. ``` % 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: ## Controlling The Server ``` % croaker start INFO Daemonizing controller on (localhost, 8003); pidfile and logs in ~/.dnd/croaker ``` Connnect to the command & control server: ```bash % telnet localhost 8003 Trying 127.0.0.1... Connected to croaker.local. Escape character is '^]'. help 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 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. ``` List available playlists: ``` list battle adventure session_start ``` Switch to battle music -- roll initiative! ``` play battle OK ``` Skip this track and move on to the next: ``` ffwd OK ``` Stop the music: ``` stop OK ``` Disconnect: ``` kthx KBAI Connection closed by foreign host. ``` ## 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') ```