Croaker is a shoutcast-compatible audio streamer written in python.
Go to file
2025-09-14 15:51:42 -07:00
src/croaker clean up dependencies 2025-09-14 15:23:15 -07:00
test clean up dependencies 2025-09-14 15:23:15 -07:00
.coveragerc adding tests 2024-03-08 13:18:25 -08:00
.gitignore Initial commit 2024-02-28 16:26:59 -08:00
LICENSE Initial commit 2024-02-28 16:26:59 -08:00
pyproject.toml clean up dependencies 2025-09-14 15:23:15 -07:00
README.md update README 2025-09-14 15:51:42 -07:00

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:

% 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:

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')