From b65e2178c5f0e959a749241e47209fc234c65715 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:43:10 +0200 Subject: [PATCH] Update DOCS.md --- birdnet-pi/DOCS.md | 376 +-------------------------------------------- 1 file changed, 2 insertions(+), 374 deletions(-) diff --git a/birdnet-pi/DOCS.md b/birdnet-pi/DOCS.md index 384b8d79b..65cfe7aa2 100644 --- a/birdnet-pi/DOCS.md +++ b/birdnet-pi/DOCS.md @@ -359,386 +359,14 @@ sudo systemctl start tmp.mount Optional : Configuration for Focusrite Scarlett 2i2 Add this content in "$HOME/focusrite.sh" && chmod +x "$HOME/focusrite.sh" -``` -#!/bin/bash +https://github.com/alexbelgium/Birdnet-tools/blob/main/focusrite.sh -# Set PCM controls for capture -sudo amixer -c 0 cset numid=31 'Analogue 1' # 'PCM 01' - Set to 'Analogue 1' -sudo amixer -c 0 cset numid=32 'Analogue 1' # 'PCM 02' - Set to 'Analogue 1' -sudo amixer -c 0 cset numid=33 'Off' # 'PCM 03' - Disabled -sudo amixer -c 0 cset numid=34 'Off' # 'PCM 04' - Disabled - -# Set DSP Input controls (Unused, set to Off) -sudo amixer -c 0 cset numid=29 'Off' # 'DSP Input 1' -sudo amixer -c 0 cset numid=30 'Off' # 'DSP Input 2' - -# Configure Line In 1 as main input for mono setup -sudo amixer -c 0 cset numid=8 'Off' # 'Line In 1 Air' - Keep 'Off' -sudo amixer -c 0 cset numid=14 off # 'Line In 1 Autogain' - Disabled -sudo amixer -c 0 cset numid=6 'Line' # 'Line In 1 Level' - Set level to 'Line' -sudo amixer -c 0 cset numid=21 on # 'Line In 1 Safe' - Enabled to avoid clipping / noise impact ? - -# Disable Line In 2 to minimize interference (if not used) -sudo amixer -c 0 cset numid=9 'Off' # 'Line In 2 Air' -sudo amixer -c 0 cset numid=17 off # 'Line In 2 Autogain' - Disabled -sudo amixer -c 0 cset numid=16 0 # 'Line In 2 Gain' - Set gain to 0 (mute) -sudo amixer -c 0 cset numid=7 'Line' # 'Line In 2 Level' - Set to 'Line' -sudo amixer -c 0 cset numid=22 off # 'Line In 2 Safe' - Disabled - -# Set Line In 1-2 controls -sudo amixer -c 0 cset numid=12 off # 'Line In 1-2 Link' - No need to link for mono -sudo amixer -c 0 cset numid=10 on # 'Line In 1-2 Phantom Power' - Enabled for condenser mics - -# Set Analogue Outputs to use the same mix for both channels (Mono setup) -sudo amixer -c 0 cset numid=23 'Mix A' # 'Analogue Output 01' - Set to 'Mix A' -sudo amixer -c 0 cset numid=24 'Mix A' # 'Analogue Output 02' - Same mix as Output 01 - -# Set Direct Monitor to off to prevent feedback -sudo amixer -c 0 cset numid=53 'Off' # 'Direct Monitor' - -# Set Input Select to Input 1 -sudo amixer -c 0 cset numid=11 'Input 1' # 'Input Select' - -# Optimize Monitor Mix settings for mono output -sudo amixer -c 0 cset numid=54 153 # 'Monitor 1 Mix A Input 01' - Set to 153 (around -3.50 dB) -sudo amixer -c 0 cset numid=55 153 # 'Monitor 1 Mix A Input 02' - Set to 153 for balanced output -sudo amixer -c 0 cset numid=56 0 # 'Monitor 1 Mix A Input 03' - Mute unused channels -sudo amixer -c 0 cset numid=57 0 # 'Monitor 1 Mix A Input 04' - -# Set Sync Status to Locked -sudo amixer -c 0 cset numid=52 'Locked' # 'Sync Status' - -echo "Mono optimization applied. Only using primary input and balanced outputs." -```
Optional : Autogain script for microphone Add this content in "$HOME/autogain.py" && chmod +x "$HOME/autogain.py" - -```python -#!/usr/bin/env python3 -""" -Dynamic Microphone Gain Adjustment Script with Interactive Calibration, -Self‑Modification, No‑Signal Reboot Logic, and a Test Mode for Real‑Time RMS Line Graph using plotext - -Usage: - ./autogain.py -> Normal dynamic gain control - ./autogain.py --calibrate -> Interactive calibration + self-modification - ./autogain.py --test -> Test mode (real-time RMS graph) -""" - -import argparse -import subprocess -import numpy as np -from scipy.signal import butter, sosfilt -import time -import re -import sys -import os - -# ---------------------- Default Configuration ---------------------- - -MICROPHONE_NAME = "Line In 1 Gain" -MIN_GAIN_DB = 30 -MAX_GAIN_DB = 38 -GAIN_STEP_DB = 3 - -# RMS thresholds -NOISE_THRESHOLD_HIGH = 0.01 -NOISE_THRESHOLD_LOW = 0.001 - -# No-signal detection -NO_SIGNAL_THRESHOLD = 1e-6 -NO_SIGNAL_COUNT_THRESHOLD = 3 -NO_SIGNAL_ACTION = "scarlett2 reboot && sudo reboot" - -SAMPLING_RATE = 48000 # 48 kHz -LOWCUT = 2000 -HIGHCUT = 8000 -FILTER_ORDER = 4 -RTSP_URL = "rtsp://192.168.178.124:8554/birdmic" -SLEEP_SECONDS = 10 - -REFERENCE_PRESSURE = 20e-6 # 20 µPa - -# Default microphone specifications (for calibration reference) -DEFAULT_SNR = 80.0 # dB -DEFAULT_SELF_NOISE = 14.0 # dB-A -DEFAULT_CLIPPING = 120.0 # dB SPL -DEFAULT_SENSITIVITY = -28.0 # dB re 1 V/Pa - -# Compute the default full-scale amplitude (used to derive default fractions) -def_full_scale = ( - REFERENCE_PRESSURE * - 10 ** (DEFAULT_CLIPPING / 20) * - 10 ** (DEFAULT_SENSITIVITY / 20) -) - -# ---------------------- Argument Parsing ---------------------- - -def parse_args(): - parser = argparse.ArgumentParser( - description="Dynamic Mic Gain Adjustment with calibration, test mode, self‑modification, and reboot logic." - ) - parser.add_argument("--calibrate", action="store_true", help="Run interactive calibration mode") - parser.add_argument("--test", action="store_true", help="Run test mode to display a real‑time RMS graph using plotext") - return parser.parse_args() - -# ---------------------- Audio & Gain Helpers ---------------------- - -def debug_print(msg, level="info"): - current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) - print(f"[{current_time}] [{level.upper()}] {msg}") - -def get_gain_db(mic_name): - try: - output = subprocess.check_output( - ['amixer', 'sget', mic_name], stderr=subprocess.STDOUT - ).decode() - match = re.search(r'\[(-?\d+(\.\d+)?)dB\]', output) - if match: - return float(match.group(1)) - except subprocess.CalledProcessError as e: - debug_print(f"amixer sget failed: {e}", "error") - return None - -def set_gain_db(mic_name, gain_db): - gain_db = max(min(gain_db, MAX_GAIN_DB), MIN_GAIN_DB) - try: - subprocess.check_call( - ['amixer', 'sset', mic_name, f'{int(gain_db)}dB'], - stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) - debug_print(f"Gain set to: {gain_db} dB", "info") - return True - except subprocess.CalledProcessError as e: - debug_print(f"Failed to set gain: {e}", "error") - return False - -def capture_audio(rtsp_url, duration=5): - cmd = [ - 'ffmpeg', '-loglevel', 'error', '-rtsp_transport', 'tcp', - '-i', rtsp_url, '-vn', '-f', 's16le', '-acodec', 'pcm_s16le', - '-ar', str(SAMPLING_RATE), '-ac', '1', '-t', str(duration), '-' - ] - try: - process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = process.communicate() - if process.returncode != 0: - debug_print(f"ffmpeg failed: {stderr.decode().strip()}", "error") - return None - return np.frombuffer(stdout, dtype=np.int16).astype(np.float32) / 32768.0 - except Exception as e: - debug_print(f"Audio capture exception: {e}", "error") - return None - -def bandpass_filter(audio, lowcut, highcut, fs, order=4): - sos = butter(order, [lowcut, highcut], btype='band', fs=fs, output='sos') - return sosfilt(sos, audio) - -def measure_rms(audio): - return float(np.sqrt(np.mean(audio**2))) if len(audio) > 0 else 0.0 - -# ---------------------- Interactive Calibration ---------------------- - -def prompt_float(prompt_str, default_val): - while True: - user_input = input(f"{prompt_str} [{default_val}]: ").strip() - if user_input == "": - return default_val - try: - return float(user_input) - except ValueError: - print("Invalid input; please enter a numeric value.") - -def interactive_calibration(): - print("\n-- INTERACTIVE CALIBRATION --") - print("Enter the microphone characteristics (press Enter to accept default):\n") - snr = prompt_float("1) Signal-to-Noise Ratio (dB)", DEFAULT_SNR) - self_noise = prompt_float("2) Self Noise (dB-A)", DEFAULT_SELF_NOISE) - clipping = prompt_float("3) Clipping SPL (dB)", DEFAULT_CLIPPING) - sensitivity = prompt_float("4) Sensitivity (dB re 1 V/Pa)", DEFAULT_SENSITIVITY) - return {"snr": snr, "self_noise": self_noise, "clipping": clipping, "sensitivity": sensitivity} - -def calibrate_and_propose(mic_params): - user_snr = mic_params["snr"] - clipping = mic_params["clipping"] - sensitivity = mic_params["sensitivity"] - - user_full_scale = ( - REFERENCE_PRESSURE * - 10 ** (clipping / 20) * - 10 ** (sensitivity / 20) - ) - fraction_high_default = NOISE_THRESHOLD_HIGH / def_full_scale - fraction_low_default = NOISE_THRESHOLD_LOW / def_full_scale - snr_ratio = user_snr / DEFAULT_SNR - - proposed_high = fraction_high_default * user_full_scale * snr_ratio - proposed_low = fraction_low_default * user_full_scale * snr_ratio - gain_offset = (DEFAULT_SENSITIVITY - sensitivity) - proposed_min_gain = MIN_GAIN_DB + gain_offset - proposed_max_gain = MAX_GAIN_DB + gain_offset - - print("\n===============================================================") - print("CURRENT VALUES:") - print("---------------------------------------------------------------") - print(f" NOISE_THRESHOLD_HIGH: {NOISE_THRESHOLD_HIGH:.7f}") - print(f" NOISE_THRESHOLD_LOW: {NOISE_THRESHOLD_LOW:.7f}") - print(f" MIN_GAIN_DB: {MIN_GAIN_DB}") - print(f" MAX_GAIN_DB: {MAX_GAIN_DB}") - print("---------------------------------------------------------------\n") - print("PROPOSED VALUES:") - print("---------------------------------------------------------------") - print(f" Proposed NOISE_THRESHOLD_HIGH: {proposed_high:.7f}") - print(f" Proposed NOISE_THRESHOLD_LOW: {proposed_low:.7f}\n") - print(" Proposed Gain Range (dB):") - print(f" MIN_GAIN_DB: {proposed_min_gain:.2f}") - print(f" MAX_GAIN_DB: {proposed_max_gain:.2f}") - print("---------------------------------------------------------------\n") - - return { - "noise_threshold_high": proposed_high, - "noise_threshold_low": proposed_low, - "min_gain_db": proposed_min_gain, - "max_gain_db": proposed_max_gain, - } - -def persist_calibration_to_script(script_path, proposal): - subs = { - "NOISE_THRESHOLD_HIGH": f"{proposal['noise_threshold_high']:.7f}", - "NOISE_THRESHOLD_LOW": f"{proposal['noise_threshold_low']:.7f}", - "MIN_GAIN_DB": f"{int(round(proposal['min_gain_db']))}", - "MAX_GAIN_DB": f"{int(round(proposal['max_gain_db']))}" - } - for var, val in subs.items(): - cmd = f"sed -i 's|^{var} = .*|{var} = {val}|' \"{script_path}\"" - os.system(cmd) - print("✅ Script has been updated with the new calibration values.\n") - -# ---------------------- Test Mode: Real-Time RMS Graph using plotext ---------------------- - -def test_mode(): - try: - import plotext as plt - except ImportError: - print("plotext is required for test mode. Please install it using 'pip install plotext'.") - sys.exit(1) - - print("\n-- TEST MODE: Real-Time RMS Line Graph (plotext) --") - print("Recording 5-second samples in a loop. Press Ctrl+C to exit.\n") - rms_history = [] - iterations = [] - max_points = 20 - i = 0 - - while True: - audio = capture_audio(RTSP_URL, duration=5) - if audio is None or len(audio) == 0: - print("No audio captured, retrying...") - time.sleep(5) - continue - - filtered = bandpass_filter(audio, LOWCUT, HIGHCUT, SAMPLING_RATE, FILTER_ORDER) - rms = measure_rms(filtered) - - rms_history.append(rms) - iterations.append(i) - i += 1 - - if len(rms_history) > max_points: - rms_history = rms_history[-max_points:] - iterations = iterations[-max_points:] - - if rms > NOISE_THRESHOLD_HIGH: - status = "🔴 ABOVE" - elif rms < NOISE_THRESHOLD_LOW: - status = "🔵 BELOW" - else: - status = "🟢 OK" - - plt.clf() - plt.plot(iterations, rms_history, marker="dot", color="cyan") - plt.horizontal_line(NOISE_THRESHOLD_HIGH, color="red") - plt.horizontal_line(NOISE_THRESHOLD_LOW, color="blue") - plt.title("Real-Time RMS (Line Graph)") - plt.xlabel("Iteration") - plt.ylabel("RMS") - plt.ylim(0, max(0.001, max(rms_history) * 1.2)) - plt.show() - - print(f"Current RMS: {rms:.6f} — {status}") - time.sleep(0.5) - -# ---------------------- Dynamic Gain Control Loop ---------------------- - -def dynamic_gain_control(): - debug_print("Starting dynamic gain controller...", "info") - set_gain_db(MICROPHONE_NAME, (MIN_GAIN_DB + MAX_GAIN_DB) // 2) - - no_signal_count = 0 - - while True: - audio = capture_audio(RTSP_URL) - if audio is None or len(audio) == 0: - debug_print("No audio captured; retrying...", "warning") - time.sleep(SLEEP_SECONDS) - continue - - filtered = bandpass_filter(audio, LOWCUT, HIGHCUT, SAMPLING_RATE, FILTER_ORDER) - rms = measure_rms(filtered) - debug_print(f"Measured RMS: {rms:.6f}", "info") - - # No-signal detection - if rms < NO_SIGNAL_THRESHOLD: - no_signal_count += 1 - debug_print(f"No signal detected ({no_signal_count}/{NO_SIGNAL_COUNT_THRESHOLD})", "warning") - if no_signal_count >= NO_SIGNAL_COUNT_THRESHOLD: - debug_print("No signal for too long, executing action...", "error") - subprocess.call(NO_SIGNAL_ACTION, shell=True) - else: - no_signal_count = 0 - - current_gain = get_gain_db(MICROPHONE_NAME) - if current_gain is None: - debug_print("Failed to read current gain; skipping cycle.", "warning") - time.sleep(SLEEP_SECONDS) - continue - - if rms > NOISE_THRESHOLD_HIGH: - set_gain_db(MICROPHONE_NAME, current_gain - GAIN_STEP_DB) - elif rms < NOISE_THRESHOLD_LOW: - set_gain_db(MICROPHONE_NAME, current_gain + GAIN_STEP_DB) - - time.sleep(SLEEP_SECONDS) - -# ---------------------- Main ---------------------- - -def main(): - args = parse_args() - - if args.calibrate: - mic_params = interactive_calibration() - proposal = calibrate_and_propose(mic_params) - save = input("Save these values permanently into the script? [y/N]: ").strip().lower() - if save in ["y", "yes"]: - persist_calibration_to_script(os.path.abspath(__file__), proposal) - print("👍 Calibration values saved. Exiting now.\n") - else: - print("❌ Not saving values. Exiting.\n") - sys.exit(0) - - if args.test: - test_mode() - sys.exit(0) - - dynamic_gain_control() - -if __name__ == "__main__": - main() - -``` +See : https://github.com/alexbelgium/Birdnet-tools/blob/main/autogain.py