feat: audio stack working — pulseaudio + x6200_control + voice_rec

- Add pulseaudio in system mode
- Add alsactl restore with correct mixer state from Aether
- Add x6200-control package (I2C hardware control library)
- Audio chain: pulseaudio -> x6200_control_init -> voice_rec -> speaker
This commit is contained in:
Joakim
2026-05-05 16:03:48 +02:00
parent a5b9efae15
commit c500e75417
12 changed files with 937 additions and 3 deletions

View File

@@ -1,7 +1,5 @@
menu "Mestre" menu "Mestre"
# Custom packages will be sourced here as they are added. source "$BR2_EXTERNAL_MESTRE_PATH/package/x6200-control/Config.in"
# Each line should look like:
# source "$BR2_EXTERNAL_MESTRE_PATH/package/<name>/Config.in"
endmenu endmenu

View File

@@ -0,0 +1,88 @@
# direct mix for pcm playback
# aplay -D mixplayback xxx.wav
pcm.mixout {
type dmix
ipc_key 1024
slave {
# slave PCM name
pcm "hw:0,0"
# PCM params
format S16_LE # STR
rate 16000 # INT
#rate 48000 # INT
channels 2 # INT
# stream params,buffer_size=period x period_size
periods 100 # INT,when buffer_size or buffer_time is not specified
period_size 1024 # INT,in frames,3 times than app need
# optional params
# period_time 0 # INT,in usec
# buffer_time 0 # INT,in usec
# buffer_size 65536 #INT,in frames
}
bindings {
0 0 # from 0 -> 0
1 1 # from 1 -> 1
}
# slowptr BOOL # slow but more precise pointer updates
}
# dsnoop for capture
# arecord -D mixcapture test.wav
pcm.mixin {
type dsnoop
ipc_key 1025 # must be unique for all dmix plugins!!!!
# ipc_key_add_uid yes # no need for embedded system
slave {
# slave PCM name
pcm "hw:0,0"
# PCM params
format S16_LE # STR
rate 16000 # INT
#rate 48000 # INT
channels 2 # INT
# stream params,buffer_size=period x period_size
periods 100 # INT,when buffer_size or buffer_time is not specified
period_size 1024 # INT,in frames,3 times than app need
# optional params
# period_time 0 # INT,in usec
# buffer_time 0 # INT,in usec
# buffer_size 65536 #INT,in frames
}
bindings {
0 0 # from 0 -> 0
1 1 # from 1 -> 1
}
}
# pcm plug for playback
pcm.mixplayback {
type plug
slave.pcm "mixout"
# A hint is required for listing the device in some GUIs
hint {
show on
description "X6200 playback mixer"
}
}
# pcm plug for capture
pcm.mixcapture {
type plug
slave.pcm "mixin"
# A hint is required for listing the device in some GUIs
hint {
show on
description "X6200 capture mixer"
}
}
ctl.mixer0 {
type hw
card 0
}

View File

@@ -0,0 +1,40 @@
#!/bin/sh
#
# Alsa config
#
source /etc/profile
start() {
printf "Restore alsa"
alsactl restore
echo "OK"
}
stop() {
printf "Store alsa"
# alsactl store
echo "OK"
}
restart() {
stop
start
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
restart
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
esac
exit $?

View File

@@ -0,0 +1,45 @@
#!/bin/sh
#
# Starts pulseaudio.
#
start() {
printf "Starting pulseaudio: "
umask 007
/usr/bin/pulseaudio \
--system \
--daemonize \
--disallow-exit \
--exit-idle-time=-1 \
--use-pid-file \
--disable-shm
echo "OK"
}
stop() {
printf "Stopping pulseaudio: "
PULSE_RUNTIME_PATH=/var/run/pulse /usr/bin/pulseaudio --kill
echo "OK"
}
restart() {
stop
start
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
restart
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
esac
exit $?

View File

@@ -0,0 +1,59 @@
#!/bin/sh
# S70mestre-audio — X6200 audio hardware init
#
# Initializes the X6200's audio routing via I2C:
# 1. Calls x6200_control_init() to establish MCU state
# 2. Sets speaker volume
# 3. Switches audio routing from RX-radio to SoC DAC (voice_rec)
#
# Must run after S50pulseaudio (pulseaudio must hold PCM open first
# to keep ALSA clocks active).
#
# The voice_rec bit (0x00008 in sple_atue_trx register, cmd 12)
# switches the X6200's internal audio mux from the RF receiver
# to the SoC DAC — allowing piHPSDR/aplay to be heard in the speaker.
VOLUME=50
do_init() {
python3 << EOF
import ctypes
lib = ctypes.CDLL("libaether_x6200_control.so")
lib.x6200_control_init.restype = ctypes.c_bool
lib.x6200_control_rxvol_set.argtypes = [ctypes.c_uint8]
lib.x6200_control_spmode_set.argtypes = [ctypes.c_bool]
if not lib.x6200_control_init():
raise SystemExit("x6200_control_init() failed")
lib.x6200_control_rxvol_set($VOLUME)
lib.x6200_control_spmode_set(False) # Speaker mode (not headphone)
EOF
}
do_voice_rec() {
# Switch audio routing: RX-radio -> SoC DAC (voice_rec bit = 0x00008)
i2ctransfer -y 0 w6@0x72 0x00 0x30 0x08 0x00 0x00 0x00
}
case "$1" in
start)
echo "Starting mestre-audio"
do_init && do_voice_rec && echo "OK" || echo "FAILED"
;;
stop)
echo "Stopping mestre-audio"
# Switch back to RX-radio audio
i2ctransfer -y 0 w6@0x72 0x00 0x30 0x00 0x00 0x00 0x00
echo "OK"
;;
restart)
$0 stop
sleep 1
$0 start
;;
*)
echo "Usage: $0 {start|stop|restart}" >&2
exit 1
;;
esac

View File

@@ -0,0 +1,23 @@
#!/bin/sh
# S96keepalive — X6200 PMU keepalive
#
# The X6200 powers itself off after ~180s unless the AXP PMU receives
# periodic I2C traffic. This script starts the keepalive daemon at boot.
case "$1" in
start)
echo "Starting keepalive"
/usr/sbin/i2c_keepalive.sh >/dev/null 2>&1 &
;;
stop)
echo "Stopping keepalive"
killall i2c_keepalive.sh 2>/dev/null || true
;;
restart)
$0 stop; sleep 1; $0 start
;;
*)
echo "Usage: $0 {start|stop|restart}" >&2
exit 1
;;
esac

View File

@@ -0,0 +1,70 @@
#!/usr/bin/pulseaudio -nF
#
# This file is part of PulseAudio.
#
# PulseAudio is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# PulseAudio is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
# This startup script is used only if PulseAudio is started in system
# mode.
### Automatically restore the volume of streams and devices
load-module module-device-restore
load-module module-stream-restore
load-module module-card-restore
load-module module-switch-on-connect
load-module module-bluez5-device
### Automatically load driver modules depending on the hardware available
.ifexists module-udev-detect.so
load-module module-udev-detect
.else
### Use the static hardware detection module (for systems that lack udev/hal support)
load-module module-detect
.endif
### Load several protocols
.ifexists module-esound-protocol-unix.so
load-module module-esound-protocol-unix
.endif
load-module module-native-protocol-unix auth-anonymous=1
### Automatically restore the default sink/source when changed by the user
### during runtime
### NOTE: This should be loaded as early as possible so that subsequent modules
### that look up the default sink/source get the right value
load-module module-default-device-restore
### Make sure we always have a sink around, even if it is a null sink.
load-module module-always-sink
### Automatically suspend sinks/sources that become idle for too long
load-module module-suspend-on-idle
### Enable positioned event sounds
load-module module-position-event-sounds
### Automatically load driver modules for Bluetooth hardware
.ifexists module-bluetooth-policy.so
load-module module-bluetooth-policy
.endif
.ifexists module-bluetooth-discover.so
load-module module-bluetooth-discover
.endif
### Allow including a system.pa.d directory, which if present, can be used
### for additional configuration snippets.
### Note that those snippet files must have a .pa file extension, not .conf
.nofail
.include /etc/pulse/system.pa.d

View File

@@ -0,0 +1,30 @@
#!/bin/sh
# i2c_keepalive.sh — X6200 AXP PMU keepalive
#
# The X6200 uses an AXP PMU on I2C bus 0, address 0x72.
# Without periodic I2C traffic the unit powers off after ~180s.
# This script sends a combined write+read transaction every second.
#
# Requires: i2c-tools (i2ctransfer)
BUS=0
ADDR=0x72
INTERVAL=1
# Only allow one instance
if pidof -x "$(basename "$0")" >/dev/null 2>&1; then
COUNT="$(pidof -x "$(basename "$0")" | wc -w)"
[ "$COUNT" -gt 1 ] && { echo "[keepalive] already running"; exit 0; }
fi
trap 'exit 0' INT TERM
# One-time kick at startup
i2ctransfer -y "$BUS" w4@"$ADDR" 0xFF 0xFE 0x02 0x80 >/dev/null 2>&1
i2ctransfer -y "$BUS" w4@"$ADDR" 0xFF 0xFE 0x03 0x80 >/dev/null 2>&1
# Main keepalive loop
while :; do
i2ctransfer -y "$BUS" w2@"$ADDR" 0x20 0x00 r1@"$ADDR" >/dev/null 2>&1
sleep "$INTERVAL"
done

View File

@@ -0,0 +1,526 @@
state.sun8ia33audio {
control.1 {
iface MIXER
name 'AIF1 AD0 Capture Volume'
value.0 160
value.1 160
comment {
access 'read write'
type INTEGER
count 2
range '0 - 192'
dbmin -9999999
dbmax 2400
dbvalue.0 0
dbvalue.1 0
}
}
control.2 {
iface MIXER
name 'AIF1 DA0 Playback Volume'
value.0 160
value.1 160
comment {
access 'read write'
type INTEGER
count 2
range '0 - 192'
dbmin -9999999
dbmax 2400
dbvalue.0 0
dbvalue.1 0
}
}
control.3 {
iface MIXER
name 'AIF2 ADC Capture Volume'
value.0 0
value.1 0
comment {
access 'read write'
type INTEGER
count 2
range '0 - 192'
dbmin -9999999
dbmax 2400
dbvalue.0 -9999999
dbvalue.1 -9999999
}
}
control.4 {
iface MIXER
name 'AIF2 DAC Playback Volume'
value.0 0
value.1 0
comment {
access 'read write'
type INTEGER
count 2
range '0 - 192'
dbmin -9999999
dbmax 2400
dbvalue.0 -9999999
dbvalue.1 -9999999
}
}
control.5 {
iface MIXER
name 'ADC Capture Volume'
value.0 160
value.1 160
comment {
access 'read write'
type INTEGER
count 2
range '0 - 192'
dbmin -9999999
dbmax 2400
dbvalue.0 0
dbvalue.1 0
}
}
control.6 {
iface MIXER
name 'DAC Playback Volume'
value.0 0
value.1 0
comment {
access 'read write'
type INTEGER
count 2
range '0 - 192'
dbmin -9999999
dbmax 2400
dbvalue.0 -9999999
dbvalue.1 -9999999
}
}
control.7 {
iface MIXER
name 'Headphone Playback Volume'
value 58
comment {
access 'read write'
type INTEGER
count 1
range '0 - 63'
dbmin -9999999
dbmax 0
dbvalue.0 -500
}
}
control.8 {
iface MIXER
name 'Headphone Playback Switch'
value.0 true
value.1 true
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.9 {
iface MIXER
name 'Line In Playback Volume'
value 0
comment {
access 'read write'
type INTEGER
count 1
range '0 - 7'
dbmin -450
dbmax 600
dbvalue.0 -450
}
}
control.10 {
iface MIXER
name 'Mic2 Playback Volume'
value 0
comment {
access 'read write'
type INTEGER
count 1
range '0 - 7'
dbmin -450
dbmax 600
dbvalue.0 -450
}
}
control.11 {
iface MIXER
name 'Mic2 Boost Volume'
value 0
comment {
access 'read write'
type INTEGER
count 1
range '0 - 7'
dbmin 0
dbmax 4200
dbvalue.0 0
}
}
control.12 {
iface MIXER
name 'Mic1 Playback Volume'
value 0
comment {
access 'read write'
type INTEGER
count 1
range '0 - 7'
dbmin -450
dbmax 600
dbvalue.0 -450
}
}
control.13 {
iface MIXER
name 'Mic1 Boost Volume'
value 0
comment {
access 'read write'
type INTEGER
count 1
range '0 - 7'
dbmin 0
dbmax 4200
dbvalue.0 0
}
}
control.14 {
iface MIXER
name 'ADC Gain Capture Volume'
value 3
comment {
access 'read write'
type INTEGER
count 1
range '0 - 7'
dbmin -450
dbmax 600
dbvalue.0 0
}
}
control.15 {
iface MIXER
name 'AIF1 AD0 Stereo Capture Route'
value.0 'Mix Mono'
value.1 'Mix Mono'
comment {
access 'read write'
type ENUMERATED
count 2
item.0 Stereo
item.1 'Reverse Stereo'
item.2 'Sum Mono'
item.3 'Mix Mono'
}
}
control.16 {
iface MIXER
name 'AIF2 ADC Stereo Capture Route'
value.0 Stereo
value.1 Stereo
comment {
access 'read write'
type ENUMERATED
count 2
item.0 Stereo
item.1 'Reverse Stereo'
item.2 'Sum Mono'
item.3 'Mix Mono'
}
}
control.17 {
iface MIXER
name 'AIF3 ADC Source Capture Route'
value None
comment {
access 'read write'
type ENUMERATED
count 1
item.0 None
item.1 'AIF2 ADCL'
item.2 'AIF2 ADCR'
}
}
control.18 {
iface MIXER
name 'AIF1 Slot 0 Digital ADC Capture Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.19 {
iface MIXER
name 'AIF2 Digital ADC Capture Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.20 {
iface MIXER
name 'AIF1 Data Digital ADC Capture Switch'
value.0 true
value.1 true
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.21 {
iface MIXER
name 'AIF2 Inv Digital ADC Capture Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.22 {
iface MIXER
name 'AIF2 ADC Mixer AIF1 DA0 Capture Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.23 {
iface MIXER
name 'AIF2 ADC Mixer AIF2 DAC Rev Capture Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.24 {
iface MIXER
name 'AIF2 ADC Mixer ADC Capture Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.25 {
iface MIXER
name 'AIF2 DAC Source Playback Route'
value AIF2
comment {
access 'read write'
type ENUMERATED
count 1
item.0 AIF2
item.1 AIF3+2
item.2 AIF2+3
}
}
control.26 {
iface MIXER
name 'AIF1 DA0 Stereo Playback Route'
value.0 'Mix Mono'
value.1 'Mix Mono'
comment {
access 'read write'
type ENUMERATED
count 2
item.0 Stereo
item.1 'Reverse Stereo'
item.2 'Sum Mono'
item.3 'Mix Mono'
}
}
control.27 {
iface MIXER
name 'AIF2 DAC Stereo Playback Route'
value.0 Stereo
value.1 Stereo
comment {
access 'read write'
type ENUMERATED
count 2
item.0 Stereo
item.1 'Reverse Stereo'
item.2 'Sum Mono'
item.3 'Mix Mono'
}
}
control.28 {
iface MIXER
name 'AIF1 Slot 0 Digital DAC Playback Switch'
value.0 true
value.1 true
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.29 {
iface MIXER
name 'AIF2 Digital DAC Playback Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.30 {
iface MIXER
name 'ADC Digital DAC Playback Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.31 {
iface MIXER
name 'DAC Playback Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.32 {
iface MIXER
name 'DAC Reversed Playback Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.33 {
iface MIXER
name 'Line In Playback Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.34 {
iface MIXER
name 'Mic1 Playback Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.35 {
iface MIXER
name 'Mic2 Playback Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.36 {
iface MIXER
name 'Mixer Capture Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.37 {
iface MIXER
name 'Mixer Reversed Capture Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.38 {
iface MIXER
name 'Line In Capture Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.39 {
iface MIXER
name 'Mic1 Capture Switch'
value.0 true
value.1 true
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.40 {
iface MIXER
name 'Mic2 Capture Switch'
value.0 false
value.1 false
comment {
access 'read write'
type BOOLEAN
count 2
}
}
control.41 {
iface MIXER
name 'Headphone Source Playback Route'
value.0 DAC
value.1 DAC
comment {
access 'read write'
type ENUMERATED
count 2
item.0 DAC
item.1 Mixer
}
}
}

View File

@@ -125,3 +125,22 @@ BR2_PACKAGE_STRACE=y
# - mestred (our evdev→MIDI service) # - mestred (our evdev→MIDI service)
# - Network Manager (using simple /etc/network/interfaces for now) # - Network Manager (using simple /etc/network/interfaces for now)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
BR2_PACKAGE_ALSA_UTILS=y
BR2_PACKAGE_ALSA_UTILS_APLAY=y
BR2_PACKAGE_ALSA_UTILS_AMIXER=y
BR2_PACKAGE_ALSA_UTILS_ALSAMIXER=y
BR2_PACKAGE_ALSA_LIB=y
BR2_PACKAGE_ALSA_LIB_MIXER=y
BR2_PACKAGE_ALSA_LIB_PCM=y
BR2_PACKAGE_ALSA_LIB_RAWMIDI=y
BR2_PACKAGE_X6200_CONTROL=y
BR2_PACKAGE_PYTHON3=y
BR2_PACKAGE_PYTHON3_CTYPES=y
BR2_PACKAGE_PULSEAUDIO=y
BR2_PACKAGE_PULSEAUDIO_DAEMON=y
BR2_PACKAGE_PULSEAUDIO_MODULE_ALSA=y
BR2_PACKAGE_PULSEAUDIO=y
BR2_PACKAGE_PULSEAUDIO_DAEMON=y
BR2_PACKAGE_PULSEAUDIO_MODULE_ALSA=y

View File

@@ -0,0 +1,12 @@
config BR2_PACKAGE_X6200_CONTROL
bool "x6200-control"
select BR2_PACKAGE_LIBGPIOD
help
Aether-Radio's base control library for the Xiegu X6200.
Provides I2C-based control of the X6200's MCU/FPGA, including
audio routing (RX/DAC switch), VFO/mode/AGC, ATU, and TX/RX
state.
Required for Mestre to drive the X6200's hardware.
https://github.com/gdyuldin/X6200Control

View File

@@ -0,0 +1,24 @@
################################################################################
#
# x6200-control
#
# Aether-Radio's base control library for the Xiegu X6200. Provides
# I2C-based control of the radio's MCU/FPGA: audio routing, VFO, mode,
# AGC, ATU, TX/RX state, etc.
#
################################################################################
X6200_CONTROL_VERSION = v0.1.0
X6200_CONTROL_SITE = https://github.com/gdyuldin/X6200Control
X6200_CONTROL_SITE_METHOD = git
X6200_CONTROL_LICENSE = LGPL-2.1-or-later
X6200_CONTROL_LICENSE_FILES = LICENSE
X6200_CONTROL_INSTALL_STAGING = YES
X6200_CONTROL_DEPENDENCIES = libgpiod
# Disable LTO; keeps the build simple and avoids cross-toolchain issues.
X6200_CONTROL_CONF_OPTS = \
-DAETHER_X6200CTRL_USE_LTO=OFF \
-DBUILD_SHARED_LIBS=ON
$(eval $(cmake-package))