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 re
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
import struct
|
||||||
import threading
|
import threading
|
||||||
from flask import Flask, Response, render_template, request, jsonify
|
from flask import Flask, Response, render_template, request, jsonify
|
||||||
from flask_sock import Sock
|
from flask_sock import Sock
|
||||||
|
|
@ -114,6 +115,30 @@ def stream():
|
||||||
AUDIO_SAMPLERATE = 16000
|
AUDIO_SAMPLERATE = 16000
|
||||||
AUDIO_CHUNK = 512 # ~32ms bei 16kHz
|
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:
|
def find_audio_device() -> str:
|
||||||
"""Findet die USB-Soundkarte dynamisch anhand des Namens."""
|
"""Findet die USB-Soundkarte dynamisch anhand des Namens."""
|
||||||
|
|
@ -148,7 +173,7 @@ def audio_ws(ws):
|
||||||
chunk = process.stdout.read(AUDIO_CHUNK)
|
chunk = process.stdout.read(AUDIO_CHUNK)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
ws.send(chunk)
|
ws.send(_apply_gain(chunk, _audio_gain))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
finally:
|
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
|
# System
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,14 @@
|
||||||
<label>Laufzeit</label>
|
<label>Laufzeit</label>
|
||||||
<div class="value">{{ status.uptime }}</div>
|
<div class="value">{{ status.uptime }}</div>
|
||||||
</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">
|
<div class="card">
|
||||||
<label>System</label>
|
<label>System</label>
|
||||||
<div style="display:flex;gap:.4rem;margin-top:.3rem;flex-wrap:wrap;">
|
<div style="display:flex;gap:.4rem;margin-top:.3rem;flex-wrap:wrap;">
|
||||||
|
|
@ -264,6 +272,31 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<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;
|
let selectedSSID = null;
|
||||||
|
|
||||||
async function scanNetworks() {
|
async function scanNetworks() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue