Доработка звука
This commit is contained in:
31
.gitattributes
vendored
Normal file
31
.gitattributes
vendored
Normal 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
158
internal/audio/monitor.go
Normal 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
61
scripts/vu_meter.py
Normal 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}")
|
||||
Reference in New Issue
Block a user