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
+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
}