209 lines
6.0 KiB
Go
209 lines
6.0 KiB
Go
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()
|
||
} |