This commit is contained in:
legop3
2026-03-17 03:11:04 -04:00
parent aaa93847e1
commit 84eaab3c9c
13 changed files with 107 additions and 79 deletions
BIN
View File
Binary file not shown.
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+51 -11
View File
@@ -17,29 +17,69 @@ pcm.dmixer {
}
}
# Software playback volume (adjust with: amixer -c0 sset 'SoftMaster' 70%)
pcm.softvol {
# TTS volume control (used by default playback path).
pcm.tts_softvol {
type softvol
slave.pcm "dmixer"
control {
name "SoftMaster"
name "TTSMaster"
card 0
}
min_dB -51.0
max_dB 0.0
min_dB -60.0
max_dB 12.0
}
# Allow clients with mismatched sample formats/rates to use the mixer.
pcm.playback {
# Horn volume control.
pcm.horn_softvol {
type softvol
slave.pcm "dmixer"
control {
name "HornMaster"
card 0
}
min_dB -60.0
max_dB 12.0
}
# Forwarded audio volume control.
pcm.forward_softvol {
type softvol
slave.pcm "dmixer"
control {
name "ForwardMaster"
card 0
}
min_dB -60.0
max_dB 12.0
}
# Per-source playback PCMs.
pcm.tts {
type plug
slave.pcm "softvol"
slave.pcm "tts_softvol"
}
# Defaults: playback through shared mixer + softvol, capture raw on the HAT
pcm.horn {
type plug
slave.pcm "horn_softvol"
}
pcm.forward {
type plug
slave.pcm "forward_softvol"
}
# Capture alias used by rover config defaults.
pcm.rovermic {
type plug
slave.pcm "hw:0,0"
}
# Defaults: TTS direct playback + raw capture on the HAT.
pcm.!default {
type asym
playback.pcm "playback"
capture.pcm "hw:0,0"
playback.pcm "tts"
capture.pcm "rovermic"
}
ctl.!default {
+1 -1
View File
@@ -13,7 +13,7 @@ fi
source "$ENV_FILE"
: "${AUDIO_FORWARD_URL:?AUDIO_FORWARD_URL not set in ${ENV_FILE}}"
PLAYBACK_DEVICE="${AUDIO_PLAYBACK_DEVICE:-default}"
PLAYBACK_DEVICE="${AUDIO_PLAYBACK_DEVICE:-forward}"
if [[ -n "${FFMPEG_BIN:-}" ]]; then
FFMPEG_BIN_PATH="$FFMPEG_BIN"
+1 -1
View File
@@ -223,7 +223,7 @@ AUDIO_FORWARD_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME-fwd,m=request
VIDEO_BITRATE=2000000
AUDIO_ENABLE=0
AUDIO_DEVICE=hw:0,0
AUDIO_PLAYBACK_DEVICE=default
AUDIO_PLAYBACK_DEVICE=forward
AUDIO_RATE=48000
AUDIO_CHANNELS=2
ENV
+39 -3
View File
@@ -1,5 +1,11 @@
package roverd
import (
"fmt"
"math"
"os/exec"
)
type AudioLevels struct {
HornGain float64
TTSGain float64
@@ -34,9 +40,7 @@ func (c *WSClient) setAudioLevels(next AudioLevels) {
c.audioMu.Lock()
c.audioLevels = normalized
c.audioMu.Unlock()
if c.horn != nil {
c.horn.SetGlobalGain(normalized.HornGain)
}
c.applyAudioLevelsToMixer(normalized)
}
func (c *WSClient) handleAudioLevels(payload *audioLevelsPayload) error {
@@ -56,3 +60,35 @@ func (c *WSClient) handleAudioLevels(payload *audioLevelsPayload) error {
c.setAudioLevels(levels)
return nil
}
func (c *WSClient) applyAudioLevelsToMixer(levels AudioLevels) {
c.applyMixerGain("HornMaster", levels.HornGain)
c.applyMixerGain("TTSMaster", levels.TTSGain)
c.applyMixerGain("ForwardMaster", levels.ForwardGain)
}
func (c *WSClient) applyMixerGain(control string, gain float64) {
normalized := clampAudioGain(gain)
if normalized <= 0 {
out, err := exec.Command("amixer", "-q", "-c", "0", "sset", control, "0%").CombinedOutput()
if err != nil {
c.log.Printf("audio-levels: amixer mute %s failed: %v (%s)", control, err, string(out))
}
return
}
// Convert linear gain to dB, matching softvol max_dB=12.0 in /etc/asound.conf.
db := 20.0 * math.Log10(normalized)
if db > 12.0 {
db = 12.0
}
if db < -60.0 {
db = -60.0
}
dbArg := fmt.Sprintf("%.2fdB", db)
out, err := exec.Command("amixer", "-q", "-c", "0", "sset", control, dbArg).CombinedOutput()
if err != nil {
c.log.Printf("audio-levels: amixer set %s=%s failed: %v (%s)", control, dbArg, err, string(out))
}
}
+3 -3
View File
@@ -172,7 +172,7 @@ func LoadConfig(path string) (*Config, error) {
Audio: AudioConfig{
CaptureEnabled: false,
CaptureDevice: "rovermic",
PlaybackDevice: "default",
PlaybackDevice: "forward",
SampleRate: 48000,
Channels: 2,
Bitrate: 24000,
@@ -314,8 +314,8 @@ func validateAudioConfig(cfg *AudioConfig) {
if cfg.CaptureEnabled && cfg.CaptureDevice == "" {
cfg.CaptureDevice = "hw:0,0"
}
if cfg.PlaybackDevice == "" {
cfg.PlaybackDevice = "default"
if cfg.PlaybackDevice == "" || cfg.PlaybackDevice == "default" {
cfg.PlaybackDevice = "forward"
}
if cfg.SampleRate <= 0 {
cfg.SampleRate = 48000
+4 -3
View File
@@ -124,10 +124,11 @@ func (h *HornSynth) run(waveform string, freqs []float64, stop <-chan struct{})
h.mu.Unlock()
volume *= gain
args := []string{"-q", "-f", "S16_LE", "-c", fmt.Sprintf("%d", channels), "-r", fmt.Sprintf("%d", rate), "-t", "raw"}
if h.cfg.Device != "" {
args = append(args, "-D", h.cfg.Device)
device := strings.TrimSpace(h.cfg.Device)
if device == "" {
device = "horn"
}
args := []string{"-q", "-D", device, "-f", "S16_LE", "-c", fmt.Sprintf("%d", channels), "-r", fmt.Sprintf("%d", rate), "-t", "raw"}
cmd := exec.Command("aplay", args...)
stdin, err := cmd.StdinPipe()
if err != nil {
+1 -1
View File
@@ -54,7 +54,7 @@ func UpdatePublisherEnv(media MediaConfig, audio AudioConfig) error {
fmt.Fprintf(&buf, "AUDIO_DEVICE=%s\n", audioDevice)
playbackDevice := audio.PlaybackDevice
if playbackDevice == "" {
playbackDevice = "default"
playbackDevice = "forward"
}
fmt.Fprintf(&buf, "AUDIO_PLAYBACK_DEVICE=%s\n", playbackDevice)
fmt.Fprintf(&buf, "AUDIO_RATE=%d\n", audio.SampleRate)
+1 -1
View File
@@ -39,7 +39,7 @@ cameraServo:
audio:
captureEnabled: false
captureDevice: hw:0,0
playbackDevice: default
playbackDevice: forward
sampleRate: 48000
channels: 2
bitrate: 24000
+5 -52
View File
@@ -3,7 +3,6 @@ package roverd
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
@@ -48,75 +47,29 @@ func (c *WSClient) handleTTSPayload(ctx context.Context, payload *ttsPayload) er
runCtx, cancel := context.WithTimeout(ctx, 12*time.Second)
defer cancel()
tmp, err := os.CreateTemp("", "roverd-tts-*.wav")
if err != nil {
return fmt.Errorf("tts temp file: %w", err)
}
tmpPath := tmp.Name()
_ = tmp.Close()
defer os.Remove(tmpPath)
if err := synthTTS(runCtx, engine, voice, pitch, text, tmpPath); err != nil {
return err
}
if err := playTTSFile(runCtx, tmpPath, c.cfg.Audio.PlaybackDevice, c.getAudioLevels().TTSGain); err != nil {
return err
}
return nil
}
func synthTTS(ctx context.Context, engine, voice string, pitch int, text, outputWavPath string) error {
var cmd *exec.Cmd
switch engine {
case "espeak", "e":
args := []string{"-w", outputWavPath}
args := []string{}
if pitch > 0 {
args = append(args, "-p", fmt.Sprintf("%d", pitch))
}
args = append(args, text)
cmd = exec.CommandContext(ctx, "espeak", args...)
cmd = exec.CommandContext(runCtx, "espeak", args...)
case "flite", "f":
args := []string{}
if voice != "" {
args = append(args, "-voice", voice)
}
args = append(args, "-t", text, "-o", outputWavPath)
cmd = exec.CommandContext(ctx, "flite", args...)
args = append(args, "-t", text)
cmd = exec.CommandContext(runCtx, "flite", args...)
default:
return fmt.Errorf("unsupported tts engine: %s", engine)
}
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("tts synth failed: %w (%s)", err, string(out))
}
return nil
}
func playTTSFile(ctx context.Context, wavPath, playbackDevice string, gain float64) error {
if playbackDevice == "" {
playbackDevice = "default"
}
gain = clampAudioGain(gain)
if gain == 0 {
// Mute is an explicit value users may choose.
return nil
}
args := []string{
"-hide_banner",
"-loglevel", "warning",
"-i", wavPath,
"-af", fmt.Sprintf("aresample=16000,volume=%g", gain),
"-ac", "1",
"-ar", "16000",
"-f", "alsa",
playbackDevice,
}
cmd := exec.CommandContext(ctx, "ffmpeg", args...)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("tts playback failed: %w (%s)", err, string(out))
return fmt.Errorf("tts exec failed: %w (%s)", err, string(out))
}
return nil
}
+1 -3
View File
@@ -64,9 +64,7 @@ func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, even
ForwardGain: 1.0,
},
}
if client.horn != nil {
client.horn.SetGlobalGain(client.audioLevels.HornGain)
}
client.applyAudioLevelsToMixer(client.audioLevels)
return client
}