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:
parent
cd133cec32
commit
27f98f049b
48
app/main.py
48
app/main.py
|
|
@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue