Доработка документации и вывода сообщений при ошибках

This commit is contained in:
Maxim
2026-07-13 09:35:42 +03:00
parent 1b305500a8
commit 48aa91b8cd
2 changed files with 132 additions and 41 deletions

View File

@@ -19,6 +19,7 @@ type Monitor struct {
mu sync.RWMutex
currentLevel int
cancel context.CancelFunc
wg sync.WaitGroup
}
func NewMonitor() *Monitor {
@@ -35,6 +36,17 @@ 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)
@@ -48,56 +60,17 @@ func (m *Monitor) Start() error {
stdout, err := cmd.StdoutPipe()
if err != nil {
cancel()
return err
}
if err := cmd.Start(); err != nil {
cancel()
return err
}
log.Printf("Аудио-монитор запущен с устройством: %s", device)
go func() {
buffer := make([]byte, 3200)
for {
select {
case <-ctx.Done():
return
default:
_, err := io.ReadFull(stdout, buffer)
if err != nil {
log.Printf("Ошибка чтения аудио: %v", err)
time.Sleep(2 * time.Second)
continue
}
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()
}
}
}()
m.wg.Add(1)
go m.readLoop(ctx, stdout)
go func() {
_ = cmd.Wait()
@@ -106,6 +79,83 @@ func (m *Monitor) Start() error {
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()
@@ -155,4 +205,5 @@ func (m *Monitor) Stop() {
if m.cancel != nil {
m.cancel()
}
m.wg.Wait()
}