diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0d9ea85 --- /dev/null +++ b/.gitattributes @@ -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 \ No newline at end of file diff --git a/internal/audio/monitor.go b/internal/audio/monitor.go new file mode 100644 index 0000000..403e22c --- /dev/null +++ b/internal/audio/monitor.go @@ -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() + } +} \ No newline at end of file diff --git a/scripts/vu_meter.py b/scripts/vu_meter.py new file mode 100644 index 0000000..66bc959 --- /dev/null +++ b/scripts/vu_meter.py @@ -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}") \ No newline at end of file