#!/usr/bin/env bash # Phase 0 · Beweis S2 — Erreicht das Mikrofon die Linux-Seite? # # Der stillste K.-o.-Punkt des Projekts. Ohne Mikrofon im Linux gibt es keine # Videocalls in Element oder im Browser, keine Sprachaufnahme, keine Spracheingabe. # # Der Test ist bewusst dreistufig, weil "man hört nichts" drei völlig # verschiedene Ursachen haben kann und man sie sonst nicht auseinanderhält: # # Stufe 1 Wiedergabe — kommt überhaupt Ton aus dem Linux heraus? # Stufe 2 Aufnahme — liefert die Mikrofonquelle Daten? # Stufe 3 Gegenhören — ist auf der Aufnahme wirklich Sprache? # # Ausführen IN TERMUX: bash spike/02-s2-mikrofon.sh set -uo pipefail source "$(dirname "$0")/lib.sh" require_termux DISTRO=debian DAUER="${HPOS_DAUER:-6}" WAV_NAME="s2-aufnahme.wav" WAV="$OUT_DIR/$WAV_NAME" log "HPOS Phase 0 · S2 — Mikrofon im Linux" log "Protokoll: $LOG" voraussetzungen_pruefen "$DISTRO" || { log "" fail "Abbruch: die Grundlage steht nicht. S2 wäre nicht aussagekräftig." exit 1 } # ── Berechtigung ───────────────────────────────────────────────────────────── # Der häufigste Grund für eine formal korrekte, aber stumme Aufnahme: PulseAudio # läuft IM Termux-Prozess und öffnet das Mikrofon über OpenSL ES von dort aus. # Eine Berechtigung, die nur Termux:API hat, nützt ihm deshalb nichts. section "Mikrofonberechtigung von Termux" BERECHTIGT="unbekannt" RECHTE=$(dumpsys package com.termux 2>/dev/null | grep -i 'RECORD_AUDIO' || true) if [ -n "$RECHTE" ]; then log "$(printf '%s' "$RECHTE" | sed 's/^/ /')" case "$RECHTE" in *granted=true*) BERECHTIGT=ja; ok "com.termux darf aufnehmen" ;; *granted=false*) BERECHTIGT=nein; fail "com.termux darf NICHT aufnehmen" ;; esac else # Erwartbar: dumpsys verlangt die DUMP-Berechtigung, die eine gewöhnliche App # nicht hat. Kein Grund zur Sorge, aber der Status bleibt hier eben offen. warn "Status von hier aus nicht auslesbar (dumpsys ist Apps verwehrt)" hint "Vom PC prüfen: adb shell dumpsys package com.termux | grep RECORD_AUDIO" hint "Setzen: adb shell pm grant com.termux android.permission.RECORD_AUDIO" hint "Oder am Gerät: Einstellungen → Apps → Termux → Berechtigungen → Mikrofon" fi if [ "$BERECHTIGT" = nein ]; then log "" fail "Das ist mit hoher Wahrscheinlichkeit die Ursache." hint "Auf dem Telefon: Einstellungen → Apps → Termux → Berechtigungen → Mikrofon" hint "Oder vom PC: adb shell pm grant com.termux android.permission.RECORD_AUDIO" hint "Danach dieses Skript erneut ausführen." log "" printf ' Trotzdem weitermachen? [j/N] > ' read -r WEITER /dev/null || true sleep 1 if pulseaudio --start --exit-idle-time=-1 \ --load="module-sles-source" \ --load="module-native-protocol-tcp auth-ip-acl=127.0.0.1 auth-anonymous=1" \ >>"$LOG" 2>&1; then ok "PulseAudio mit module-sles-source gestartet" else warn "module-sles-source ließ sich nicht laden — starte ohne" try pulseaudio --start --exit-idle-time=-1 \ --load="module-native-protocol-tcp auth-ip-acl=127.0.0.1 auth-anonymous=1" fi sleep 2 section "Quellen und Standardgeräte" if have pactl; then pactl info 2>&1 | grep -Ei 'Default (Source|Sink)' | tee -a "$LOG" | sed 's/^/ /' log " Quellen:" pactl list sources short 2>&1 | tee -a "$LOG" | sed 's/^/ /' # Die Standardquelle ist bei zwei Einträgen gern der Sink-Monitor. Der nimmt # auf, was ABGESPIELT wird, nicht was am Mikrofon anliegt — eine Aufnahme # daraus sieht technisch einwandfrei aus und enthält doch nie eine Stimme. STANDARD=$(pactl info 2>/dev/null | awk -F': ' '/Default Source/{print $2}') case "$STANDARD" in *monitor*) warn "Standardquelle ist ein Monitor ($STANDARD) — deshalb wird gleich" hint "ausdrücklich OpenSL_ES_source gewählt statt der Vorgabe." ;; esac fi # ── Stufe 1: Wiedergabe ────────────────────────────────────────────────────── section "Stufe 1 — kommt Ton heraus?" log " Gleich sollte ein kurzer Ton zu hören sein. Lautstärke aufdrehen." sleep 1 proot-distro login "$DISTRO" --shared-tmp --bind "$REPO_ROOT:/hpos" -- bash -lc ' export PULSE_SERVER=127.0.0.1 command -v sox >/dev/null 2>&1 || apt-get install -y --no-install-recommends sox >/dev/null 2>&1 if command -v sox >/dev/null 2>&1; then sox -n -r 44100 -c 1 /tmp/ton.wav synth 1.5 sine 440 vol 0.4 2>/dev/null paplay /tmp/ton.wav && echo " Ton abgespielt." else echo " ! sox nicht verfügbar, Wiedergabetest übersprungen" fi ' 2>&1 | tee -a "$LOG" printf '\n War der Ton zu hören? [j/N] > ' read -r TON_GEHOERT /dev/null | awk "/OpenSL_ES_source/ {print \$2; exit}") if [ -z "$QUELLE" ]; then QUELLE=$(pactl list sources short 2>/dev/null | awk "!/monitor/ {print \$2; exit}") fi if [ -n "$QUELLE" ]; then echo " Aufnahmequelle: $QUELLE" ARG="-d $QUELLE" else echo " ! Keine Nicht-Monitor-Quelle gefunden — nehme die Vorgabe." ARG="" fi timeout $((HPOS_DAUER + 4)) parecord $ARG --channels=1 --rate=16000 \ --file-format=wav "$HPOS_WAV" & REC=$! sleep "$HPOS_DAUER" kill $REC 2>/dev/null wait $REC 2>/dev/null echo " Aufnahme beendet." ' 2>&1 | tee -a "$LOG" # ── Auswertung ─────────────────────────────────────────────────────────────── section "Auswertung" if [ ! -s "$WAV" ]; then fail "Keine Aufnahmedatei entstanden → S2 gescheitert" else ok "Datei: $WAV ($(stat -c%s "$WAV" 2>/dev/null || echo 0) Bytes)" if have python; then python - "$WAV" <<'PY' 2>&1 | tee -a "$LOG" import array, math, sys, wave # Bewusst ohne audioop: das Modul ist in Python 3.13 entfernt worden. try: with wave.open(sys.argv[1]) as w: breite = w.getsampwidth() rate = w.getframerate() roh = w.readframes(w.getnframes()) if breite == 1: werte = array.array("b", bytes(b - 128 for b in roh)) elif breite == 2: werte = array.array("h", roh[: len(roh) // 2 * 2]) else: raise ValueError(f"{breite} Byte je Abtastwert werden hier nicht ausgewertet") if not werte: raise ValueError("keine Abtastwerte enthalten") pegel = int(math.sqrt(sum(v * v for v in werte) / len(werte))) spitze = max(abs(v) for v in werte) print(f" Effektivpegel: {pegel} Spitze: {spitze}") # Sprache schwankt stark: laute Silben, Pausen dazwischen. Ein konstanter # Pegel deutet auf Brummen, Rauschen oder ein Störsignal hin — technisch ein # Signal, aber keine Aufnahme. Deshalb wird die Schwankung mitgemessen. fenster = max(1, rate // 20) bloecke = [werte[i:i + fenster] for i in range(0, len(werte), fenster)] pegel_je_block = [ int(math.sqrt(sum(v * v for v in b) / len(b))) for b in bloecke if b ] if len(pegel_je_block) > 4: leise = min(pegel_je_block) laut = max(pegel_je_block) print(f" Leiseste Stelle: {leise} Lauteste: {laut}") if spitze < 40: print(" ✗ Praktisch Stille — die Quelle liefert keine Daten.") elif laut > 4 * max(leise, 1): print(" ✓ Deutliche Schwankung — sieht nach Sprache aus.") else: print(" ! Signal ohne Schwankung — eher Rauschen oder Brummen als Sprache.") print(" Möglicherweise wurde ein Monitor statt des Mikrofons aufgenommen.") elif spitze < 40: print(" ✗ Praktisch Stille — die Quelle liefert keine Daten.") else: print(" ✓ Signal vorhanden.") except Exception as e: print(f" ! Datei nicht auswertbar: {e}") PY fi # ── Stufe 3: Gegenhören ───────────────────────────────────────────────── # Der einzige Test, der wirklich zählt. Kein Messwert ersetzt das Ohr. section "Stufe 3 — Gegenhören" log " Die Aufnahme wird jetzt abgespielt." sleep 1 HPOS_WAV="/hpos/spike/out/$WAV_NAME" \ proot-distro login "$DISTRO" --shared-tmp --bind "$REPO_ROOT:/hpos" -- bash -lc ' export PULSE_SERVER=127.0.0.1 paplay "$HPOS_WAV" && echo " Wiedergabe beendet." ' 2>&1 | tee -a "$LOG" fi log "" log " Berechtigung com.termux: $BERECHTIGT" log " Ton vorher gehört: ${TON_GEHOERT:-n}" verdict "S2" "War auf der Wiedergabe deine Stimme zu hören? Nur das zählt. Ein Messwert ohne hörbare Sprache ist ein Fehlschlag."