mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
audio
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package roverd
|
||||
|
||||
type AudioLevels struct {
|
||||
HornGain float64
|
||||
TTSGain float64
|
||||
ForwardGain float64
|
||||
}
|
||||
|
||||
func clampAudioGain(v float64) float64 {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
if v > 4 {
|
||||
return 4
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func normalizeAudioLevels(v AudioLevels) AudioLevels {
|
||||
v.HornGain = clampAudioGain(v.HornGain)
|
||||
v.TTSGain = clampAudioGain(v.TTSGain)
|
||||
v.ForwardGain = clampAudioGain(v.ForwardGain)
|
||||
return v
|
||||
}
|
||||
|
||||
func (c *WSClient) getAudioLevels() AudioLevels {
|
||||
c.audioMu.RLock()
|
||||
defer c.audioMu.RUnlock()
|
||||
return c.audioLevels
|
||||
}
|
||||
|
||||
func (c *WSClient) setAudioLevels(next AudioLevels) {
|
||||
normalized := normalizeAudioLevels(next)
|
||||
c.audioMu.Lock()
|
||||
c.audioLevels = normalized
|
||||
c.audioMu.Unlock()
|
||||
if c.horn != nil {
|
||||
c.horn.SetGlobalGain(normalized.HornGain)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *WSClient) handleAudioLevels(payload *audioLevelsPayload) error {
|
||||
if payload == nil {
|
||||
return nil
|
||||
}
|
||||
levels := c.getAudioLevels()
|
||||
if payload.HornGain != nil {
|
||||
levels.HornGain = clampAudioGain(*payload.HornGain)
|
||||
}
|
||||
if payload.TTSGain != nil {
|
||||
levels.TTSGain = clampAudioGain(*payload.TTSGain)
|
||||
}
|
||||
if payload.ForwardGain != nil {
|
||||
levels.ForwardGain = clampAudioGain(*payload.ForwardGain)
|
||||
}
|
||||
c.setAudioLevels(levels)
|
||||
return nil
|
||||
}
|
||||
@@ -29,6 +29,7 @@ type inboundMessage struct {
|
||||
Servo *servoPayload `json:"servo,omitempty"`
|
||||
TTS *ttsPayload `json:"tts,omitempty"`
|
||||
Horn *hornPayload `json:"horn,omitempty"`
|
||||
AudioLevels *audioLevelsPayload `json:"audioLevels,omitempty"`
|
||||
NightVision *nightVisionPayload `json:"nightVision,omitempty"`
|
||||
Song *songPayload `json:"song,omitempty"`
|
||||
Reboot *rebootPayload `json:"reboot,omitempty"`
|
||||
@@ -73,6 +74,12 @@ type hornPayload struct {
|
||||
Freqs []float64 `json:"freqs,omitempty"`
|
||||
}
|
||||
|
||||
type audioLevelsPayload struct {
|
||||
HornGain *float64 `json:"hornGain,omitempty"`
|
||||
TTSGain *float64 `json:"ttsGain,omitempty"`
|
||||
ForwardGain *float64 `json:"forwardGain,omitempty"`
|
||||
}
|
||||
|
||||
type nightVisionPayload struct {
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
+16
-4
@@ -18,8 +18,9 @@ const (
|
||||
)
|
||||
|
||||
type HornSynth struct {
|
||||
cfg HornConfig
|
||||
log *log.Logger
|
||||
cfg HornConfig
|
||||
log *log.Logger
|
||||
gain float64
|
||||
|
||||
mu sync.Mutex
|
||||
stop chan struct{}
|
||||
@@ -29,11 +30,18 @@ type HornSynth struct {
|
||||
|
||||
func NewHornSynth(cfg HornConfig, logger *log.Logger) *HornSynth {
|
||||
return &HornSynth{
|
||||
cfg: cfg,
|
||||
log: logger,
|
||||
cfg: cfg,
|
||||
log: logger,
|
||||
gain: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HornSynth) SetGlobalGain(gain float64) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.gain = clampAudioGain(gain)
|
||||
}
|
||||
|
||||
func (h *HornSynth) HandlePayload(payload *hornPayload) error {
|
||||
if payload == nil {
|
||||
return fmt.Errorf("horn payload required")
|
||||
@@ -111,6 +119,10 @@ func (h *HornSynth) run(waveform string, freqs []float64, stop <-chan struct{})
|
||||
if volume > 1 {
|
||||
volume = 1
|
||||
}
|
||||
h.mu.Lock()
|
||||
gain := h.gain
|
||||
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 != "" {
|
||||
|
||||
+52
-5
@@ -3,6 +3,7 @@ package roverd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -47,29 +48,75 @@ 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{}
|
||||
args := []string{"-w", outputWavPath}
|
||||
if pitch > 0 {
|
||||
args = append(args, "-p", fmt.Sprintf("%d", pitch))
|
||||
}
|
||||
args = append(args, text)
|
||||
cmd = exec.CommandContext(runCtx, "espeak", args...)
|
||||
cmd = exec.CommandContext(ctx, "espeak", args...)
|
||||
case "flite", "f":
|
||||
args := []string{}
|
||||
if voice != "" {
|
||||
args = append(args, "-voice", voice)
|
||||
}
|
||||
args = append(args, "-t", text)
|
||||
cmd = exec.CommandContext(runCtx, "flite", args...)
|
||||
args = append(args, "-t", text, "-o", outputWavPath)
|
||||
cmd = exec.CommandContext(ctx, "flite", args...)
|
||||
default:
|
||||
return fmt.Errorf("unsupported tts engine: %s", engine)
|
||||
}
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("tts exec failed: %w (%s)", err, string(out))
|
||||
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 nil
|
||||
}
|
||||
|
||||
+14
-1
@@ -34,6 +34,8 @@ type WSClient struct {
|
||||
rebootT *time.Timer
|
||||
seekIssued bool
|
||||
rebootIssued bool
|
||||
audioLevels AudioLevels
|
||||
audioMu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, events chan RoverEvent, media *MediaSupervisor, servo *CameraServo, nightVision *NightVisionLight, logger *log.Logger) *WSClient {
|
||||
@@ -45,7 +47,7 @@ func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, even
|
||||
if cfg.Horn.Enabled {
|
||||
horn = NewHornSynth(cfg.Horn, logger)
|
||||
}
|
||||
return &WSClient{
|
||||
client := &WSClient{
|
||||
cfg: cfg,
|
||||
adapter: adapter,
|
||||
sensorFrames: frames,
|
||||
@@ -56,7 +58,16 @@ func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, even
|
||||
nightVision: nightVision,
|
||||
log: logger,
|
||||
ttsQueue: ttsQueue,
|
||||
audioLevels: AudioLevels{
|
||||
HornGain: 1.0,
|
||||
TTSGain: 1.0,
|
||||
ForwardGain: 1.0,
|
||||
},
|
||||
}
|
||||
if client.horn != nil {
|
||||
client.horn.SetGlobalGain(client.audioLevels.HornGain)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *WSClient) Run(ctx context.Context) error {
|
||||
@@ -200,6 +211,8 @@ func (c *WSClient) dispatch(ctx context.Context, msg *inboundMessage) error {
|
||||
return fmt.Errorf("horn disabled")
|
||||
}
|
||||
return c.horn.HandlePayload(msg.Horn)
|
||||
case msg.AudioLevels != nil:
|
||||
return c.handleAudioLevels(msg.AudioLevels)
|
||||
case msg.NightVision != nil:
|
||||
if c.nightVision == nil {
|
||||
return fmt.Errorf("night vision disabled")
|
||||
|
||||
Reference in New Issue
Block a user