Skip to content

Instantly share code, notes, and snippets.

@scyto
Last active December 26, 2025 20:52
Show Gist options
  • Select an option

  • Save scyto/2eef6306f3ac0e90252d21b35c0e6473 to your computer and use it in GitHub Desktop.

Select an option

Save scyto/2eef6306f3ac0e90252d21b35c0e6473 to your computer and use it in GitHub Desktop.
CasaTunes Conversion to Music Assistant

v0.1 2025-12-23

this has not been validated end-to-end by anyone including me, so be warned :-)

ToDo

  • try using shairport instead of squeezelite as the client
  • try using sendspin instead of squeezelite (currently onnly supports one output format so not an option yet)

CasaTunes Conversion to Music Assistant

image

Why?

I have a CasaTunes music server, i like the software and the app on the iphone to control works well. The server is a CT-8x8ms but everytime someone does a good CasaTunes Home Assistant integration it gets abandoned and CasaTunes seems to have no interest.

Machine Specification:

  • Intel(R) Celeron(R) CPU J1900 @ 1.99GHz)
  • 8 GB of SODIMM RAM (i upgraded it from the stock 4GB ages ago)
  • 1TB spinning 2.5 HDD, replaced in this project with 512GB SATA SSD i had hanging around
  • Onboard ACL892 8 analog channel sound card (typically configiured for 5.1 and 2 or 3 inputs, when counting the digital input/outpu)
  • Asus Xonar DSX card with 4 outs and 1 input (also a 8 channel card, but doe

Music Assistant

The latest Music Assistant seesm to do all i want:

  • Recieve Airplay Stream
  • Send to Airplay Devices
  • Send to Sonos Devices
  • Send to a varity of other software and hardware players like squeezelite and sendspin
  • Supports Apple Music, Tidal, Spotify etc (not all with HQ though
  • Provided as Home Assistant Add-on (this gist set assumes it will be run on a seperate home assistant box, not on this server as it has no way to cofnigure or use the soundcards directly) - also means i can use home assistant as a single app for WAF
  • Supports multiple users
  • Supports mulitpkle user music acccounts (so i get my apple music, my eife gets hers)

So the question is - can i convert the box into an endpoint for Music Assistant in someway.... (answer when i finished is yes - works quite well)

Steps Overview

  • Install Debian Trixie Server without GUI - just SSH
  • Install a bunch of pre-requisities for sound card and create an audio user
  • Install enough X and VNC to be able to use hdjackretask one time to retask the blue input socket on the ALC892 onboard sound to be an output - this creates a fw file that is applied at boot and there is no CLI version of the tool AFAIK
  • Create an ALSA /etc/asound.conf for both cards
  • Install squeezelite and conigure it run 8 instances (one per zone)

Install Debian

  • Create USB of Debian and start install
  • I prefer text mode install and always disable the installation of all items except SSH and debian tools - but you do you
  • partition as you see fit
  • i am bad and do things as root when i first do these things, but yeah put your user in the sudoers file / edit visduo as needed
  • enable SSH for root if desired (yeah i do that, i know i shouldn't)
  • i kept my first run through using default networking and dhcp, but installing nmtui and networkmanager is probably the better option these days (i will likely do that later)

So at this point you should be able to login as SSH and run commands as root or sudo as your user - if not, do not go any further and get that all working first.

Install Pre-reqs

Items needed for base debian manipulation apt get update && apt get install -Y sudo

Items needed to get ALSA working with the two cards apt get install -Y alsa-base alsa-utils libasound2 libasound2-plugins psmisc squeezelite

What each was used for:

  • alsa-base
    • Card detection
    • Kernel module defaults
  • alsa-utils
    • aplay
    • arecord
    • amixer
    • alsactl
    • speaker-test ← critical
  • libasound2
    • Core ALSA userspace library
  • libasound2-plugins
    • dmix
    • route
    • plug
    • multi
    • asym
  • psmisc
    • fuser and other diag tools i used
  • squeezelite
  • all the things needed for squeezelite

Identify Sound Cards

Identify cards using aplay -l

root@music-server:~# aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: DSX [Xonar DSX], device 0: Multichannel [Multichannel]
  Subdevices: 0/1
  Subdevice #0: subdevice #0
card 1: PCH [HDA Intel PCH], device 0: ALC892 Analog [ALC892 Analog]
  Subdevices: 0/1
  Subdevice #0: subdevice #0

Note: i am only showing the analog devices, i removed the digital ones (spdif, HDMI, etc for calrity)

This lets you know that the card indexes are in my case (may be different in your case) plughw:0,0 for the Xonar plughw:1,0 for the ALC892

You can now test each channel like so

speaker-test -D plughw:1,0 -c 8 -t pink -s N

Where N is a number between 1 and 8 representing either a L or R channel - this will help you build a mental map of which HW channel is which speaker and from which stero pair.

Note: some codecs or drivers may not expose all 8 channels on a given physical jack; unused channels may be silent. I had this issue with the blue connector on the ACL892 - see below for how i resovled with hdjack retask.

create audio users

This creates an audio user for squeezelite and will be used later

sudo adduser --system --group --no-create-home squeezelite
sudo usermod -aG audio squeezelite
sudo install -d -o squeezelite -g squeezelite /var/log/squeezelite
sudo install -d -o root -g root /etc/squeezelite

hdjack-retask (optional)

This was needed in my system because when i was testing output to the ALC892 blue socket i was getting no sound as it was a)marked as unused and b)marked as an input. If you don't have this issue then you don't need to do these steps.

Items needed for the hdjackretask process and connecting via VNC, may be optional for other installs apt alsa-tools-gui xorg xinit openbox xfvb x11vnc

Create a VNC password Create a password file for x11vnc: x11vnc -storepasswd

This will prompt for a password and save it to: ~/.vnc/passwd

Start VNC sever

Xvfb :0 -screen 0 1280x800x24 &
export DISPLAY=:0
openbox &
x11vnc \
  -display :0 \
  -rfbauth ~/.vnc/passwd \
  -forever &

Connect to VNC

right click in window and start a terminal and run hdajackretask

Actions performed in the GUI:

  • Enable advanced override
  • Reassign the ALC892 blue jack from input / unused
  • Mark it as line-out
  • Apply the override and install boot override

in the terminal or an SSH test the output works

  • speaker-test -D plughw:1,0 -c 2 -t wav

Notes:

plughw:1,0 targets the ALC892 card directly, bypasses dmix and custom PCMs -c 2 Stereo output -t wavUses WAV samples (very obvious if sound is present)

👉 If you hear “Front Left / Front Right” from the blue jack, the override is working.

Set mixer levels

Run sudo alsamixer For both cards make sure the channels we will be using are unmuted and set volume to full exit run sudo alsactl store

how to ceate an asound.conf file

Ok this is where it gets complicated and confusing - how to construct an /etc/asound.conf to create zone devices applications can connect to and to make sure the right channels are mapped into the right stereo pairs for each zone. The mapping you created earlier will be important and you can refer to my asound.conf attached to the gist as a file (see below)

Background - Understand ALSA ttable routing (key mental model)

ALSA route works like this:

input stream channels → ttable → hardware channels

Think of ttable as a matrix: ttable.<input_channel>.<output_channel> = gain

For stereo input:

  • Input channel 0 = Left
  • Input channel 1 = Right

Hardware channels:

  • 0–7 from your speaker-test discovery

HW Section

These create the raw hardware defintions, one for each card based on the card index you discovered earlier

pcm.xonar_hw_raw {
    type hw
    card 0
    device 0
}

pcm.alc892_hw_raw {
    type hw
    card 1
    device 0
}

Mixer Section - this is a mixer that provides access to all 8 channels and sets things like frequency and bit rate

# 8ch dmix per card (dmix must slave to hw)
pcm.xonar_dmix8 {
    type dmix
    ipc_key 10240
    ipc_perm 0666
    slave {
        pcm "xonar_hw_raw"
        rate 96000
        format S32_LE
        channels 8
        period_time 0
        period_size 1024
        buffer_size 8192
    }
}

# 8ch dmix per card (dmix must slave to hw)
pcm.alc892_dmix8 {
    type dmix
    ipc_key 10241
    ipc_perm 0666
    slave {
        pcm "alc892_hw_raw"
        rate 96000
        format S32_LE
        channels 8
        period_time 0
        period_size 1024
        buffer_size 8192
    }
}

Routing Section

Created stero pair routes that map the channels to create a raw route (if you used this you would only be able to use a fixed frequency input, these routes are used by the plugs that create the devices you will use for applications)

# Raw route (no plug here)
pcm.zone1_raw { type route slave { pcm "xonar_dmix8"; channels 8; } ttable.0.0 1  ttable.1.1 1 }
pcm.zone2_raw { type route slave { pcm "xonar_dmix8"; channels 8; } ttable.0.6 1  ttable.1.7 1 }
pcm.zone3_raw { type route slave { pcm "xonar_dmix8"; channels 8; } ttable.0.4 1  ttable.1.5 1 }
pcm.zone4_raw { type route slave { pcm "xonar_dmix8"; channels 8; } ttable.0.2 1  ttable.1.3 1 }

# Raw route (no plug here)
pcm.zone5_raw { type route slave { pcm "alc892_dmix8"; channels 8; } ttable.0.0 1  ttable.1.1 1 }
pcm.zone6_raw { type route slave { pcm "alc892_dmix8"; channels 8; } ttable.0.2 1  ttable.1.3 1 }
pcm.zone7_raw { type route slave { pcm "alc892_dmix8"; channels 8; } ttable.0.4 1  ttable.1.5 1 }
pcm.zone8_raw { type route slave { pcm "alc892_dmix8"; channels 8; } ttable.0.6 1  ttable.1.7 1 }

so to take the first line as an example

pcm.zone1_raw { type route slave { pcm "xonar_dmix8"; channels 8; } ttable.0.0 1 ttable.1.1 1 }

this uses the ALSA route plugin to map incoming 2-channel audio so that channel 0 / left is sent to channel 0 on the card and 1 / right is sent to channel 1 on the card, both at full volume 1, via the shared Xonar mixer.

Plug Devices

This is what applications will connect to, these appear as sound devices.

# What apps open (plug does the conversion of played format to 96k/S32)
pcm.zone1 { type plug slave.pcm "zone1_raw" }
pcm.zone2 { type plug slave.pcm "zone2_raw" }
pcm.zone3 { type plug slave.pcm "zone3_raw" }
pcm.zone4 { type plug slave.pcm "zone4_raw" }
pcm.zone5 { type plug slave.pcm "zone5_raw" }
pcm.zone6 { type plug slave.pcm "zone6_raw" }
pcm.zone7 { type plug slave.pcm "zone7_raw" }
pcm.zone8 { type plug slave.pcm "zone8_raw" }

Checking all items were created

using a play you can see if all items were created correctly, it is important you only ever connect applications to zone1 through zone8 - never connect at app to the dmix or raw variants

note: i truncated the output to focus on what we created, you will see many more entries from real hardware

root@music-server:~# squeezelite -l
Output devices:
  ...
  xonar_dmix8                   
  zone1_raw                     
  zone2_raw                     
  zone3_raw                     
  zone4_raw                     
  zone1                         
  zone2                         
  zone3                         
  zone4                         
  alc892_dmix8                  
  zone5_raw                     
  zone6_raw                     
  zone7_raw                     
  zone8_raw                     
  zone5                         
  zone6                         
  zone7                         
  zone8          
  ...

8 Squeezelite instances

Create System Unit

Create the template unit: /etc/systemd/system/[email protected]

[Unit]
Description=Squeezelite (%i)
After=network-online.target sound.target
Wants=network-online.target

[Service]
Type=simple
User=squeezelite
Group=squeezelite

# One config file per instance name, e.g. /etc/squeezelite/zone1.conf
EnvironmentFile=/etc/squeezelite/%i.conf
Environment=EXTRA_OPTS=

ExecStart=/usr/bin/squeezelite \
  -n "${NAME}" \
  -m "${MAC}" \
  -o "${OUTPUT}" \
  -a "${ALSA_PARAMS}" \
  ${EXTRA_OPTS}

Restart=on-failure
RestartSec=2

# Log per zone
StandardOutput=append:/var/log/squeezelite/%i.log
StandardError=append:/var/log/squeezelite/%i.log

[Install]
WantedBy=multi-user.target

Notes:

%i will be zone1, zone2, etc.

NAME can contain spaces if quoted properly in the .conf files.

Create a conf file per zone

Only showing one example, make sure to set the last digit in the mac address to the zonenuber (like 1 or 2 or 3 etc). The squeezelite will discover the server if there is an mDNS brodcast path on the network, if you dont have that you can sepcify the server IPv4 address using the EXTRA_OPTS

/etc/squeezelite/zone1.conf

# Note if the zone name has a space use the format NAME="Room\ Area" or systemd parsing of conf file will fail
NAME="Room\ Area"
OUTPUT="zone1"
MAC="02:00:00:00:00:01"
ALSA_PARAMS="40:4::0"
# example if you need to specify the server
# EXTRA_OPTS="-s 192.168.1.63"

# note not sure if a \ will be needed after -s havent tested

Set Permission on Files

sudo chown -R root:root /etc/squeezelite
sudo chmod 0644 /etc/squeezelite/*.conf

Enable Service and start service instances

sudo systemctl daemon-reload
sudo systemctl enable squeezelite@zone{1..8}
sudo systemctl start squeezelite@zone{1..8}

List all loaded / running zone instances

check alll services are ok

systemctl list-units 'squeezelite@zone*'

should look like this

root@music-server:/etc# systemctl list-units 'squeezelite@zone*'
  UNIT                      LOAD   ACTIVE SUB     DESCRIPTION        
  [email protected] loaded active running Squeezelite (zone1)
  [email protected] loaded active running Squeezelite (zone2)
  [email protected] loaded active running Squeezelite (zone3)
  [email protected] loaded active running Squeezelite (zone4)
  [email protected] loaded active running Squeezelite (zone5)
  [email protected] loaded active running Squeezelite (zone6)
  [email protected] loaded active running Squeezelite (zone7)
  [email protected] loaded active running Squeezelite (zone8)

Legend: LOAD   → Reflects whether the unit definition was properly loaded.
        ACTIVE → The high-level unit activation state, i.e. generalization of SUB.
        SUB    → The low-level unit activation state, values depend on unit type.

8 loaded units listed. Pass --all to see loaded but inactive units, too.
To show all installed unit files use 'systemctl list-unit-files'.

for more commands see the operational guide below

root@music-server:/etc# cat asound.conf
# Xonar (card 0, device 0): plughw:0,0
# ch0=z1 L, ch1=z1 R
# ch2=z4 L, ch3=z4 R
# ch4=z3 L, ch5=z3 R
# ch6=z2 L, ch7=z2 R
#
# ALC892 (card 1, device 0): plughw:1,0
# ch0=z5 L, ch1=z5 R
# ch2=z6 L, ch3=z6 R
# ch4=z7 L, ch5=z7 R
# ch6=z8 L, ch7=z8 R
# --- Xonar Config Start ---
# Raw hardware (MUST be type hw for dmix)
pcm.xonar_hw_raw {
type hw
card 0
device 0
}
# 8ch dmix per card (dmix must slave to hw)
pcm.xonar_dmix8 {
type dmix
ipc_key 10240
ipc_perm 0666
slave {
pcm "xonar_hw_raw"
rate 96000
format S32_LE
channels 8
period_time 0
period_size 1024
buffer_size 8192
}
}
# Raw route (no plug here)
pcm.zone1_raw { type route slave { pcm "xonar_dmix8"; channels 8; } ttable.0.0 1 ttable.1.1 1 }
pcm.zone2_raw { type route slave { pcm "xonar_dmix8"; channels 8; } ttable.0.6 1 ttable.1.7 1 }
pcm.zone3_raw { type route slave { pcm "xonar_dmix8"; channels 8; } ttable.0.4 1 ttable.1.5 1 }
pcm.zone4_raw { type route slave { pcm "xonar_dmix8"; channels 8; } ttable.0.2 1 ttable.1.3 1 }
# What apps open (plug does the conversion of played format to 96k/S32)
pcm.zone1 { type plug slave.pcm "zone1_raw" }
pcm.zone2 { type plug slave.pcm "zone2_raw" }
pcm.zone3 { type plug slave.pcm "zone3_raw" }
pcm.zone4 { type plug slave.pcm "zone4_raw" }
# --- Xonar Config End ---
# --- ALC892 Config Start ---
# Raw hardware (MUST be type hw for dmix)
pcm.alc892_hw_raw {
type hw
card 1
device 0
}
# 8ch dmix per card (dmix must slave to hw)
pcm.alc892_dmix8 {
type dmix
ipc_key 10241
ipc_perm 0666
slave {
pcm "alc892_hw_raw"
rate 96000
format S32_LE
channels 8
period_time 0
period_size 1024
buffer_size 8192
}
}
# Raw route (no plug here)
pcm.zone5_raw { type route slave { pcm "alc892_dmix8"; channels 8; } ttable.0.0 1 ttable.1.1 1 }
pcm.zone6_raw { type route slave { pcm "alc892_dmix8"; channels 8; } ttable.0.2 1 ttable.1.3 1 }
pcm.zone7_raw { type route slave { pcm "alc892_dmix8"; channels 8; } ttable.0.4 1 ttable.1.5 1 }
pcm.zone8_raw { type route slave { pcm "alc892_dmix8"; channels 8; } ttable.0.6 1 ttable.1.7 1 }
# What apps open (plug does the conversion of played format to 96k/S32)
pcm.zone5 { type plug slave.pcm "zone5_raw" }
pcm.zone6 { type plug slave.pcm "zone6_raw" }
pcm.zone7 { type plug slave.pcm "zone7_raw" }
pcm.zone8 { type plug slave.pcm "zone8_raw" }
# --- ALC892 Config End ---

Squeezelite systemd Template Instance Operations Guide

This document is a practical reference for managing systemd template service instances using [email protected] on a headless Debian-based music server.

It assumes:

  • A template unit: /etc/systemd/system/[email protected]
  • Per-zone instances: squeezelite@zone1squeezelite@zone8
  • Per-zone ALSA outputs (e.g. zone1, zone2, …)
  • Per-zone logging to /var/log/squeezelite/zoneX.log

🔍 Runtime State (What’s Running Now)

List all loaded / running zone instances

systemctl list-units 'squeezelite@zone*'

Check a specific zone

systemctl status squeezelite@zone3

Show all failed services

systemctl --failed

🧩 Boot Enablement (Will It Start on Reboot?)

Check if a specific zone is enabled systemctl is-enabled squeezelite@zone3

Check enablement via status header systemctl status squeezelite@zone3 | sed -n '1,12p'

List the symlinks that cause zones to start at boot ls -l /etc/systemd/system/multi-user.target.wants/ | grep 'squeezelite@zone'

Template instances do not have individual unit files on disk. Enablement is represented by symlinks to [email protected].

▶️ Start / Stop / Restart Zones

Start or stop a single zone

sudo systemctl start squeezelite@zone3
sudo systemctl stop squeezelite@zone3

Restart a single zone sudo systemctl restart squeezelite@zone3

Restart all zones sudo systemctl restart squeezelite@zone{1..8}

🔁 Failure Handling

Reset a failed zone sudo systemctl reset-failed squeezelite@zone3

Reset all failed zone instances sudo systemctl reset-failed 'squeezelite@zone*'

🧪 Inspect systemd Expansion & Environment

Show which unit file is used systemctl show squeezelite@zone3 -p FragmentPath -p DropInPaths

Show environment variables loaded for a zone systemctl show squeezelite@zone3 -p Environment --no-pager

Show which EnvironmentFile was used systemctl show squeezelite@zone3 -p EnvironmentFiles --no-pager

📝 Logs

View journald logs for a zone journalctl -u squeezelite@zone3 --no-pager

Follow logs live journalctl -u squeezelite@zone3 -f

View per-zone log file (file-based logging) tail -f /var/log/squeezelite/zone3.log

⚙️ Unit Configuration

Show the template unit systemctl cat [email protected]

Show the instantiated unit (after substitutions) systemctl cat squeezelite@zone3

🔄 Enable / Disable Zones at Boot

Enable all zones sudo systemctl enable squeezelite@zone{1..8}

Disable a specific zone sudo systemctl disable squeezelite@zone3

🧠 Sanity Checks

Show the default boot target systemctl get-default

Confirm audio group membership for the service user id squeezelite

⭐ Minimal “Daily Use” Command Set

If you only remember a few commands, remember these:

systemctl list-units 'squeezelite@zone*'
systemctl status squeezelite@zone3
systemctl is-enabled squeezelite@zone3
systemctl reset-failed squeezelite@zone3
journalctl -u squeezelite@zone3
tail -f /var/log/squeezelite/zone3.log

These cover ~95% of operational and debugging needs.

Notes

Template instances ([email protected]) are runtime-generated.

systemctl list-unit-files will not list individual instances.

Use list-units, status, and is-enabled to inspect instance state.

For ALSA issues, always test as the service user:

sudo -u squeezelite squeezelite ...

#!/usr/bin/env bash
set -euo pipefail
DEV="${1:-plughw:0,0}" # Xonar: plughw:0,0 ; ALC892: plughw:1,0
RATE=48000
CH=8
HOLD=10
GAP=2
FREQ=440
AMP=12000
# Define which 8ch indices represent the four stereo pairs.
# Based on your mapping:
# Xonar: (0,1)=Z1 (2,3)=Z4 (4,5)=Z3 (6,7)=Z2
# ALC892: (0,1)=Z5 (2,3)=Z6 (4,5)=Z7 (6,7)=Z8
#
# This script labels them Pair0..Pair3; you’ll map them to zones while listening.
pairs=( "0 1" "2 3" "4 5" "6 7" )
play_one_channel () {
local hot="$1" # which ALSA channel index (0..7) gets tone
python3 - <<PY | aplay -q -D "$DEV" -c "$CH" -r "$RATE" -f S16_LE -t raw
import math, sys
rate=$RATE
secs=$HOLD
ch=$CH
hot=$hot
freq=$FREQ
amp=$AMP
total=int(rate*secs)
for n in range(total):
s=int(amp*math.sin(2*math.pi*freq*n/rate))
frame=[0]*ch
frame[hot]=s
for v in frame:
sys.stdout.buffer.write(int(v).to_bytes(2,'little',signed=True))
PY
}
i=0
for p in "${pairs[@]}"; do
LCH=$(echo "$p" | awk '{print $1}')
RCH=$(echo "$p" | awk '{print $2}')
echo "=== Pair $i: LEFT channel index $LCH for ${HOLD}s on $DEV ==="
play_one_channel "$LCH"
sleep "$GAP"
echo "=== Pair $i: RIGHT channel index $RCH for ${HOLD}s on $DEV ==="
play_one_channel "$RCH"
sleep "$GAP"
i=$((i+1))
done
echo "Done."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment