Add microphone gain control with live slider on dashboard

- Software gain applied per chunk in audio_ws via struct pack/unpack (S16_LE, clamped)
- Default gain 2.0 (200%) – compensates for quiet USB mics
- GET/POST /api/audio-gain to read and persist gain to /var/lib/babycam/audio-gain
- index.html: Mikrofon-Lautstärke slider card (0–500%), updates live with 300ms debounce
- Gain survives service restarts (loaded from file at startup)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Julian Vollmer 2026-05-18 18:57:28 +02:00
parent cd133cec32
commit 27f98f049b
2 changed files with 80 additions and 1 deletions

View File

@ -10,6 +10,7 @@ import subprocess
import re
import time
import os
import struct
import threading
from flask import Flask, Response, render_template, request, jsonify
from flask_sock import Sock
@ -114,6 +115,30 @@ def stream():
AUDIO_SAMPLERATE = 16000
AUDIO_CHUNK = 512 # ~32ms bei 16kHz
GAIN_FILE = "/var/lib/babycam/audio-gain"
_audio_gain: float = 2.0 # Default 200 % USB-Mics sind oft leise
def _load_audio_gain():
global _audio_gain
try:
with open(GAIN_FILE) as f:
_audio_gain = max(0.0, min(10.0, float(f.read().strip())))
except Exception:
pass
_load_audio_gain()
def _apply_gain(chunk: bytes, gain: float) -> bytes:
"""Verstärkt 16-bit PCM LE in-place (clamped)."""
if abs(gain - 1.0) < 0.005:
return chunk
n = len(chunk) // 2
samples = struct.unpack(f"<{n}h", chunk)
return struct.pack(f"<{n}h", *(max(-32768, min(32767, int(s * gain))) for s in samples))
def find_audio_device() -> str:
"""Findet die USB-Soundkarte dynamisch anhand des Namens."""
@ -148,7 +173,7 @@ def audio_ws(ws):
chunk = process.stdout.read(AUDIO_CHUNK)
if not chunk:
break
ws.send(chunk)
ws.send(_apply_gain(chunk, _audio_gain))
except Exception:
pass
finally:
@ -300,6 +325,27 @@ def wifi_connect():
})
# ---------------------------------------------------------------------------
# Audio-Gain API
# ---------------------------------------------------------------------------
@app.route("/api/audio-gain", methods=["GET"])
def api_audio_gain_get():
return jsonify({"gain": _audio_gain})
@app.route("/api/audio-gain", methods=["POST"])
def api_audio_gain_set():
global _audio_gain
data = request.get_json()
value = max(0.0, min(10.0, float(data.get("gain", 1.0))))
_audio_gain = value
os.makedirs(os.path.dirname(GAIN_FILE), exist_ok=True)
with open(GAIN_FILE, "w") as f:
f.write(str(value))
return jsonify({"success": True, "gain": _audio_gain})
# ---------------------------------------------------------------------------
# System
# ---------------------------------------------------------------------------

View File

@ -237,6 +237,14 @@
<label>Laufzeit</label>
<div class="value">{{ status.uptime }}</div>
</div>
<div class="card">
<label>Mikrofon-Lautstärke</label>
<div style="display:flex;align-items:center;gap:.75rem;margin-top:.5rem;">
<input type="range" id="gain-slider" min="0" max="500" step="10" value="200"
style="flex:1;accent-color:#1976d2;cursor:pointer;">
<span id="gain-label" style="font-size:.95rem;font-weight:500;min-width:3.5rem;text-align:right;">200%</span>
</div>
</div>
<div class="card">
<label>System</label>
<div style="display:flex;gap:.4rem;margin-top:.3rem;flex-wrap:wrap;">
@ -264,6 +272,31 @@
</div>
<script>
// Mikrofon-Gain
(async () => {
try {
const res = await fetch("/api/audio-gain");
const { gain } = await res.json();
const pct = Math.round(gain * 100);
document.getElementById("gain-slider").value = pct;
document.getElementById("gain-label").textContent = pct + "%";
} catch (e) {}
})();
let gainTimer;
document.getElementById("gain-slider").addEventListener("input", function () {
const pct = parseInt(this.value);
document.getElementById("gain-label").textContent = pct + "%";
clearTimeout(gainTimer);
gainTimer = setTimeout(() => {
fetch("/api/audio-gain", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ gain: pct / 100 }),
});
}, 300);
});
let selectedSSID = null;
async function scanNetworks() {