Доработка звука

This commit is contained in:
Maxim
2026-07-03 09:20:18 +03:00
parent 594a62a9f6
commit 19c1919fc5
3 changed files with 250 additions and 0 deletions

31
.gitattributes vendored Normal file
View File

@@ -0,0 +1,31 @@
# 1. Глобальное правило по умолчанию
# Назначает автоматическую обработку текста: в репозитории ВСЕГДА будет LF.
* text=auto eol=lf
# 2. Явное указание текстовых файлов для исходного кода и конфигураций
*.go text eol=lf
*.ts text eol=lf
*.js text eol=lf
*.json text eol=lf
*.html text eol=lf
*.css text eol=lf
*.md text eol=lf
*.sh text eol=lf
*.conf text eol=lf
*.service text eol=lf
*.socket text eol=lf
*.mod text eol=lf
*.py text eol=lf
*.c text eol=lf
*.h text eol=lf
Makefile text eol=lf
makefile text eol=lf
# 3. Исключения для бинарных файлов (чтобы Git не пытался менять в них байты)
*.png binary
*.woff2 binary
*.woff binary
*.ttf binary
*.gz binary
# папка build и файлы внутри debian часто содержат бинарники или генерируемые файлы
build/* binary

158
internal/audio/monitor.go Normal file
View File

@@ -0,0 +1,158 @@
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
}
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
// Используем гибкий поиск 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 {
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()
}
}
}()
go func() {
_ = cmd.Wait()
}()
return nil
}
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()
}
}

61
scripts/vu_meter.py Normal file
View File

@@ -0,0 +1,61 @@
import sys
import numpy as np
import sounddevice as sd
# Настройки
DURATION = 0.2 # Увеличили буфер для стабильности на Raspberry Pi
SAMPLE_RATE = 44100 # Частота дискретизации
def audio_callback(indata, frames, time, status):
# Игнорируем input overflow, чтобы не засорять вывод,
# но выводим другие критические ошибки, если они будут
if status and not status.input_overflow:
print(f"\nСтатус: {status}", file=sys.stderr)
# Вычисляем среднеквадратичное значение (RMS)
rms = np.sqrt(np.mean(indata**2))
# Переводим в проценты.
# Коэффициент 250 хорошо подходит для стандартных микрофонов
volume_percent = min(int(rms * 250), 100)
# Рисуем текстовый индикатор
bar_length = int(volume_percent / 2) # Шкала из 50 символов max
bar = "" * bar_length + "-" * (50 - bar_length)
# Выводим в консоль с очисткой конца строки (\033[K), чтобы убрать артефакты
sys.stdout.write(f"\rУровень: {volume_percent:3}% | [{bar}]\033[K")
sys.stdout.flush()
print("Поиск аудиоустройств...")
device_id = None
devices = sd.query_devices()
for idx, dev in enumerate(devices):
# Ищем устройство без учета регистра
if "pcm2902" in dev["name"].lower() and dev["max_input_channels"] > 0:
device_id = idx
print(f"Найдено устройство: {dev['name']} (ID: {idx})")
break
if device_id is None:
print("Используем аудиоустройство по умолчанию.")
print("Запуск мониторинга уровня звука. Для выхода нажмите Ctrl+C\n")
try:
with sd.InputStream(
device=device_id,
channels=1,
samplerate=SAMPLE_RATE,
blocksize=int(SAMPLE_RATE * DURATION),
callback=audio_callback,
):
while True:
sd.sleep(100)
except KeyboardInterrupt:
print("\nМониторинг остановлен.")
except Exception as e:
print(f"\nОшибка: {e}")