Files
go-service/internal/audio/monitor.go

209 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package audio
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"log"
"math"
"os/exec"
"regexp"
"strings"
"sync"
"time"
)
type Monitor struct {
mu sync.RWMutex
currentLevel int
cancel context.CancelFunc
wg sync.WaitGroup
}
func NewMonitor() *Monitor {
return &Monitor{}
}
func (m *Monitor) GetLevel() int {
m.mu.RLock()
defer m.mu.RUnlock()
return m.currentLevel
}
func (m *Monitor) Start() error {
ctx, cancel := context.WithCancel(context.Background())
m.cancel = cancel
if err := m.startCapture(ctx); err != nil {
cancel()
return err
}
return nil
}
// startCapture запускает arecord и горутину чтения потока. Может вызываться
// повторно при пересборке пайпа после сбоя (см. handleCrash).
func (m *Monitor) startCapture(ctx context.Context) error {
// Используем гибкий поиск USB устройств
device := m.findUSBDevice()
log.Printf("Используем аудиоустройство: %s", device)
cmd := exec.CommandContext(ctx, "arecord",
"-D", device,
"-t", "raw",
"-c", "1",
"-r", "16000",
"-f", "S16_LE")
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
log.Printf("Аудио-монитор запущен с устройством: %s", device)
m.wg.Add(1)
go m.readLoop(ctx, stdout)
go func() {
_ = cmd.Wait()
}()
return nil
}
func (m *Monitor) readLoop(ctx context.Context, stdout io.ReadCloser) {
defer m.wg.Done()
buffer := make([]byte, 3200)
for {
select {
case <-ctx.Done():
return
default:
}
_, err := io.ReadFull(stdout, buffer)
if err != nil {
// Контекст отменён (Stop()) — пайп закрыт намеренно, тихо выходим.
if ctx.Err() != nil {
return
}
// Дескриптор закрыт (например, cmd.Wait() уже реаппнул процесс) —
// arecord умер сам по себе, пробуем восстановиться.
if strings.Contains(err.Error(), "file already closed") {
log.Printf("Аудио-пайп закрыт: %v", err)
} else {
log.Printf("Ошибка чтения аудио: %v", err)
}
m.handleCrash(ctx)
return
}
samplesCount := len(buffer) / 2
samples := make([]int16, samplesCount)
reader := bytes.NewReader(buffer)
if err := binary.Read(reader, binary.LittleEndian, &samples); err != nil {
continue
}
var sum float64
for _, sample := range samples {
val := float64(sample)
sum += val * val
}
rms := math.Sqrt(sum / float64(samplesCount))
level := int((rms / 32768.0) * 100 * 4)
if level > 100 {
level = 100
}
m.mu.Lock()
m.currentLevel = level
m.mu.Unlock()
}
}
// handleCrash пытается пересобрать пайп arecord после того, как микрофон
// отвалился на физическом уровне, не роняя всё приложение.
func (m *Monitor) handleCrash(ctx context.Context) {
if ctx.Err() != nil {
return
}
log.Println("Микрофон отключился, повторная попытка через 5 секунд...")
select {
case <-ctx.Done():
return
case <-time.After(5 * time.Second):
}
if ctx.Err() != nil {
return
}
if err := m.startCapture(ctx); err != nil {
log.Printf("Не удалось пересобрать аудио-пайп: %v", err)
}
}
func (m *Monitor) findUSBDevice() string {
cmd := exec.Command("arecord", "-l")
output, err := cmd.Output()
if err != nil {
log.Printf("Не удалось получить список устройств: %v", err)
return "default"
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
// Ищем любое USB аудиоустройство
lowerLine := strings.ToLower(line)
if strings.Contains(lowerLine, "usb") &&
(strings.Contains(lowerLine, "audio") || strings.Contains(lowerLine, "sound")) {
log.Printf("Найдено USB аудиоустройство: %s", line)
// Извлекаем номера карты и устройства
re := regexp.MustCompile(`card (\d+):.*device (\d+):`)
matches := re.FindStringSubmatch(line)
if len(matches) == 3 {
device := fmt.Sprintf("plughw:%s,%s", matches[1], matches[2])
log.Printf("Используем устройство: %s", device)
return device
}
}
}
// Если не нашли USB, пробуем найти любое устройство захвата
for _, line := range lines {
if strings.Contains(line, "device") && strings.Contains(line, "Capture") {
re := regexp.MustCompile(`card (\d+):.*device (\d+):`)
matches := re.FindStringSubmatch(line)
if len(matches) == 3 {
device := fmt.Sprintf("plughw:%s,%s", matches[1], matches[2])
log.Printf("Используем первое найденное устройство: %s", device)
return device
}
}
}
log.Println("Аудиоустройства не найдены, используем default")
return "default"
}
func (m *Monitor) Stop() {
if m.cancel != nil {
m.cancel()
}
m.wg.Wait()
}