Skip to content

saegey/aswitch

Repository files navigation

Aswitch: Raspberry Pi Audio Source Switcher + 12V Amp Trigger

Aswitch is a Raspberry Pi-controlled audio switching and amplifier trigger system. It allows a Raspberry Pi streamer/DAC and an analog mixer to share a single amplifier input, while also controlling a 12V trigger output for powering the amplifier on/off.

Core Features

  • Switch between two stereo line-level sources:
    • Source A: Analog mixer / vinyl setup
    • Source B: Raspberry Pi USB DAC / streamer
  • Route selected stereo source to amplifier RCA input
  • Control an external amplifier via 12V trigger
  • MQTT control and state publishing for Home Assistant integration
  • Mixer/vinyl is the safe default source (relay de-energized)
  • Pi kept physically separate from audio enclosure to reduce noise

Files

Services — aswitch.local

File Purpose
aswitch.py GPIO relay control — switches audio source and 12V trigger via MQTT
audio_activity.py USB audio RMS detector — publishes active/inactive state, optional WAV recording
deploy/aswitch.service systemd unit for relay control
deploy/audio_activity.service systemd unit for audio activity detection

Services — pi-cam.local

File Purpose
dac_status.py USB DAC presence detector — polls lsusb and publishes MQTT state
deploy/dac_status.service systemd unit for DAC presence detection

pi-cam.local runs only dac_status.py. There is no relay switching, GPIO wiring, or audio recording on that host — amp control is handled by the ESPHome IR device.

Deploy tooling

File Purpose
deploy/deploy.sh rsync + venv + systemd restart over SSH
deploy/push_env.sh push .env to a host and optionally restart services
deploy/deploy_shairport_config.sh push tracked Shairport Sync config

Hardware Overview

Main Components

  • Raspberry Pi 3B
  • USB DAC for streamer output
  • Behringer UFO202 / USB Audio CODEC (audio activity detection)
  • Electronics-Salon DPDT Signal Relay Module, 5VDC (RY5W-K) — stereo source switching
  • 5V Tongling / JQC-3FF relay module — 12V trigger switching
  • 12V DC power supply for amplifier trigger
  • 6 RCA jacks: Mixer L/R in, DAC L/R in, Amplifier L/R out
  • 3.5mm mono TS jack for amplifier trigger output

Audio Signal Routing

The audio relay switches only the RCA center conductors. RCA grounds are never switched.

Left Channel

Mixer L  -> NC1
DAC L    -> NO1
Output L <- COM1

Right Channel

Mixer R  -> NC2
DAC R    -> NO2
Output R <- COM2

Default Behavior

Relay de-energized: Mixer -> Amp
Relay energized:    DAC   -> Amp

RCA Grounding

All RCA sleeve/ground connections tie to one shared audio ground bus:

Mixer RCA grounds ─┐
DAC RCA grounds   ─┼─> Shared audio ground bus
Output RCA grounds─┘

Rules:

  • Do not switch RCA ground through the relay
  • Do not use relay control ground as the audio return path
  • Tie Pi/control ground to audio ground at one point only if required
  • Use short, solid soldered or terminal connections

GPIO Wiring

GPIO pins use BCM numbering (AUDIO_PIN = 17, TRIGGER_PIN = 27).

Note: Relay polarity may vary by module. In this build both relays behaved opposite to the initial active-low assumption. Active/inactive states are defined as constants in aswitch.py.

Audio Relay — Electronics-Salon DPDT Board

Pi Pin Signal Relay
Pin 2 / 5V VCC Relay VCC
Pin 6 / GND GND Relay GND
Pin 11 / GPIO17 Control Relay IN

Trigger Relay — 5V Tongling / JQC-3FF

Pi Pin Signal Relay
Pin 2 / 5V VCC Relay DC+
Pin 6 / GND GND Relay DC-
Pin 13 / GPIO27 Control Relay IN

12V Trigger Wiring

The trigger relay switches the positive leg of the 12V supply:

12V supply +  -> Relay COM
Relay NO      -> 3.5mm TS jack TIP
12V supply -  -> 3.5mm TS jack SLEEVE

Expected output:

Relay off -> 0V at trigger jack
Relay on  -> +12V at trigger jack

Verify with a multimeter (DC voltage, red to TIP, black to SLEEVE):

Amp off -> ~0V
Amp on  -> ~+12V

MQTT Topics

aswitch.py — Relay Control

Topic Direction Payloads
aswitch/audio command dac, mixer
aswitch/audio/state state (retained) dac, mixer
aswitch/trigger command on, off
aswitch/trigger/state state (retained) on, off

audio_activity.py — Audio Monitor

Topic Direction Payloads
aswitch/audio_activity/state state (retained) active, inactive
aswitch/audio_activity/rms debug RMS float string
aswitch/audio_recording/set command on, off
aswitch/audio_recording/state state (retained) on, off
aswitch/audio_recording/file state (retained) current WAV path
aswitch/audio_recording/error event error message string

dac_status.py — DAC Presence

Topic Direction Payloads
aswitch/dac/zd3/state state (retained) connected, disconnected
aswitch/dac/zd3/details state (retained) matching lsusb line
aswitch/dac/zd3/availability availability (retained) online, offline

Home Assistant Integration

Full MQTT config: home_assistant/mqtt_sensors.yaml

mqtt:
  switch:
    - name: "Amp Power"
      unique_id: aswitch_amp_power
      command_topic: "aswitch/trigger"
      state_topic: "aswitch/trigger/state"
      payload_on: "on"
      payload_off: "off"

    - name: "Audio Source DAC"
      unique_id: aswitch_audio_source_dac
      command_topic: "aswitch/audio"
      state_topic: "aswitch/audio/state"
      payload_on: "dac"
      payload_off: "mixer"

    - name: "Mixer Recording"
      unique_id: "aswitch_mixer_recording"
      command_topic: "aswitch/audio_recording/set"
      state_topic: "aswitch/audio_recording/state"
      payload_on: "on"
      payload_off: "off"

  binary_sensor:
    - name: "Mixer Audio Activity"
      unique_id: "aswitch_mixer_audio_activity"
      state_topic: "aswitch/audio_activity/state"
      payload_on: "active"
      payload_off: "inactive"
      device_class: sound

    - name: "Fosi ZD3 Status"
      unique_id: "fosi_zd3_status"
      state_topic: "aswitch/dac/zd3/state"
      payload_on: "connected"
      payload_off: "disconnected"
      availability_topic: "aswitch/dac/zd3/availability"
      payload_available: "online"
      payload_not_available: "offline"
      device_class: connectivity
      icon: "mdi:usb-port"

Audio Source DAC ON = DAC/streamer selected; OFF = mixer/vinyl selected.

Fosi ZD3 Status is driven by dac_status.py on pi-cam.local and is used by the ZD3 automation to know whether the amp is already on before sending an IR power command.

Tracked automations:

Audio Activity Detector

audio_activity.py listens to the Behringer UFO202 (plughw:CARD=CODEC,DEV=0), computes RMS across stereo input, and applies hold timers before publishing state changes.

The UFO202 identifies as:

08bb:2902 Texas Instruments PCM2902 Audio Codec

Useful ALSA commands:

arecord -l
arecord -L
arecord -D plughw:CARD=CODEC,DEV=0 -f cd -d 5 test.wav

Activity Defaults

Constant Default Description
RMS_THRESHOLD 0.01 RMS level that counts as audio
ACTIVE_HOLD_SECONDS 2.0 Sustained above threshold before publishing active
INACTIVE_HOLD_SECONDS 300.0 Sustained below threshold before publishing inactive

Tune these on the Pi via .env if your mixer output level differs.

Recording Defaults

Env var Default Description
RECORDINGS_DIR ./recordings WAV output directory
RECORDING_ATTENUATION_DB 0.0 Gain applied before writing (e.g. -6.0)
BLOCKSIZE 8192 PortAudio block size
STREAM_LATENCY high PortAudio latency hint

DAC Status Publisher

dac_status.py polls lsusb on an interval and publishes retained MQTT state. Useful for triggering Home Assistant automations when a USB DAC is connected or disconnected.

Relevant .env keys:

DAC_USB_ID=152a:889b
DAC_MATCH_TEXT=Fosi Audio ZD3
DAC_POLL_INTERVAL_SECONDS=5
DAC_STATE_TOPIC=aswitch/dac/zd3/state
DAC_DETAILS_TOPIC=aswitch/dac/zd3/details
DAC_AVAILABILITY_TOPIC=aswitch/dac/zd3/availability

Shairport Sync

Shairport Sync is used for AirPlay streaming to the USB DAC.

sudo systemctl restart shairport-sync
sudo systemctl status shairport-sync
journalctl -u shairport-sync -f

DAC discovery:

aplay -l
aplay -L

Deploy the tracked Shairport Sync config:

ASWITCH_HOST=pi-cam.local \
./deploy/deploy_shairport_config.sh

Install Notes

System packages on Raspberry Pi OS:

sudo apt update
sudo apt install -y python3-venv python3-dev libportaudio2 portaudio19-dev libasound2-dev

Python packages:

python3 -m venv .venv
. .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt

Create the .env file on the Pi — copy from the template and fill in your credentials:

cp .env.example /home/YOUR_USER/aswitch/.env
$EDITOR /home/YOUR_USER/aswitch/.env

See .env.example for all available variables.

Deploy

The deploy script rsyncs Python files and requirements.txt, creates or reuses .venv, installs deps, and restarts the target service.

chmod +x deploy/deploy.sh
./deploy/deploy.sh                          # aswitch service -> aswitch.local
ASWITCH_SERVICE=audio_activity.service \
ASWITCH_SERVICE_TEMPLATE=audio_activity.service \
./deploy/deploy.sh                          # audio_activity service
ASWITCH_HOST=pi-cam.local \
ASWITCH_SERVICE=dac_status.service \
ASWITCH_SERVICE_TEMPLATE=dac_status.service \
./deploy/deploy.sh                          # dac_status service -> pi-cam.local

Override any default:

ASWITCH_HOST=aswitch.local \
ASWITCH_USER=pi \
ASWITCH_REMOTE_DIR=/home/pi/aswitch \
ASWITCH_SERVICE=aswitch.service \
./deploy/deploy.sh

Environment Management

Template files:

Create local secret files (gitignored):

cp env/aswitch.example.env env/aswitch.env
cp env/pi-cam.example.env env/pi-cam.env

Push env to a host:

ASWITCH_HOST=pi-cam.local \
ASWITCH_ENV_FILE=env/pi-cam.env \
./deploy/push_env.sh

Push env and restart specific services:

ASWITCH_HOST=pi-cam.local \
ASWITCH_ENV_FILE=env/pi-cam.env \
ASWITCH_RESTART_SERVICES="dac_status.service,audio_activity.service" \
./deploy/push_env.sh

Check Status On The Pi

ssh pi@aswitch.local
sudo systemctl status aswitch.service
sudo systemctl status audio_activity.service
journalctl -u aswitch.service -f
journalctl -u audio_activity.service -f

Second Room — pi-cam.local (Fosi ZD3 + ESPHome IR)

The pi-cam.local host is a separate room audio system. It is architecturally distinct from aswitch.local: there is no relay switching or GPIO wiring. Instead, amp control goes over IR via an M5Stack Atom Echo running ESPHome.

Components

  • Raspberry Pi running dac_status.py (monitors USB DAC presence via lsusb)
  • Fosi ZD3 integrated amp — controlled via IR remote
  • M5Stack Atom Echo (ESP32) running ESPHome — emits NEC IR codes to the ZD3
  • Shairport Sync for AirPlay streaming to the ZD3

ESPHome IR Controller

Config: esphome/fosi-zd3-ir.yaml

The Atom Echo emits NEC IR codes on GPIO12. All ZD3 IR commands use address 0x01FA:

Button NEC Command
Power 0xFA05
Mute 0xFD02
Volume Up 0xFC03
Volume Down 0xF807
Display Toggle 0xFE01
USB 0xEE11
Optical 0xF708
Coax 0xF609
HDMI 0xEC13
Bluetooth 0xEA15
Play/Pause 0xF50A
Next 0xF906
Previous 0xFB04

ESPHome secrets (Wi-Fi credentials, API key) go in esphome/secrets.yaml (gitignored). Copy from the template:

cp esphome/secrets.yaml.example esphome/secrets.yaml
$EDITOR esphome/secrets.yaml

Note on GPIO12: The Atom Echo IR pin may vary by hardware revision. If GPIO12 does not transmit, use an external Grove IR emitter instead.

Home Assistant Automations

Tracked automation: home_assistant/zd3_auto_power.yaml

ZD3: Auto power/input from lounge streamer — driven by media_player.lounge_streamer state changes, with three branches:

Trigger TV state ZD3 state Action
Streamer → playing any off Power on ZD3, switch to USB input, mute TV if on
Streamer → playing any on Switch to USB input, mute TV if on
Streamer → idle/off (3 min) on on Switch ZD3 to HDMI, unmute TV
Streamer → idle/off (3 min) off on Switch ZD3 to HDMI, then power off ZD3

Key entities to replace for your setup:

Placeholder What it maps to
media_player.lounge_streamer Your AirPlay / streamer media player entity
media_player.lounge_tv Your TV media player entity
remote.lounge_tv Your TV remote (used to read on/off state)
binary_sensor.fosi_zd3_status DAC presence sensor from dac_status.py MQTT
button.fosi_zd3_ir_zd3_* ESPHome IR buttons from fosi-zd3-ir.yaml

The automation uses mode: restart so rapid streamer state changes retrigger cleanly rather than queuing.

Pi-cam Deploy

dac_status.py runs on pi-cam.local. Deploy and push env the same way as other services:

ASWITCH_HOST=pi-cam.local \
ASWITCH_SERVICE=dac_status.service \
ASWITCH_SERVICE_TEMPLATE=dac_status.service \
./deploy/deploy.sh
ASWITCH_HOST=pi-cam.local \
ASWITCH_ENV_FILE=env/pi-cam.env \
./deploy/push_env.sh

Enclosure Notes

Recommended final enclosure:

  • Metal project enclosure for shielding and mechanical stability
  • Pi kept in a separate enclosure
  • Audio switcher box contains: RCA jacks, audio relay, trigger relay, 3.5mm trigger jack, ground bus

Prototype enclosure:

  • 3D printed PETG or PLA
  • Optional copper/aluminum tape shielding (connect to audio ground bus at one point)
  • Use posts, tie-downs, and wire routing guides to keep RCA ground wiring short and solid

PCB V2 Notes

A future PCB could replace hand wiring and include:

  • 6 PCB or panel-mounted RCA jacks
  • DPDT 5V signal relay for stereo source switching
  • Trigger relay circuit
  • 3.5mm trigger output
  • GPIO/control header
  • 5V/GND distribution
  • Audio ground plane / bus
  • Optional chassis ground point
  • Optional on-board audio activity detector circuit

PCB traces are acceptable for line-level audio. Suggested layout rules:

  • Short traces; solid ground plane
  • Keep relay coil/control traces away from audio traces
  • Switch only signal — never RCA ground
  • Route left/right symmetrically where practical
  • 20–30 mil traces for audio signals

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors