From f08399ba1d957dddfb5a5a4320aaaef4067db9c8 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 9 Aug 2026 20:23:21 +0200 Subject: [PATCH] =?UTF-8?q?S2:=20RECORD=5FAUDIO=20f=C3=BCr=20Termux=20selb?= =?UTF-8?q?st,=20Quelle=20explizit,=20Diagnose=20dreistufig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aus dem ersten Gerätelauf: module-sles-source lädt, die Aufnahme meldet einen kräftigen Pegel (Spitze 21658) — und trotzdem ist nichts zu hören. Drei Fehler kamen dabei zusammen. 1. provision.sh vergab RECORD_AUDIO nur an com.termux.api. PulseAudio läuft aber IM Termux-Prozess und öffnet das Mikrofon von dort über OpenSL ES. Die Berechtigung muss com.termux haben; sonst lädt das Modul zwar, liefert aber nichts Brauchbares. S2 prüft den Status jetzt vorab und nennt beide Wege, ihn zu setzen. 2. parecord lief ohne -d und nahm damit die Standardquelle. Bei geladenem Sink-Modul ist das häufig dessen Monitor — der zeichnet auf, was abgespielt wird, nicht was am Mikrofon anliegt. Eine solche Aufnahme sieht technisch einwandfrei aus und enthält doch nie eine Stimme. Jetzt wird OpenSL_ES_source ausdrücklich gewählt. 3. 'Man hört nichts' hat drei mögliche Ursachen, die der Test nicht getrennt hat. Er läuft jetzt dreistufig: erst ein Testton (kommt überhaupt Ton heraus?), dann die Aufnahme, dann automatisches Gegenhören. Ohne Stufe 1 sagt ein stummes Gegenhören nichts aus. Die Auswertung misst zusätzlich die Schwankung des Pegels. Sprache hat laute Silben und Pausen dazwischen; ein gleichmäßiges Signal ist Brummen oder ein Monitor-Mitschnitt. Gegen Stille, Brummen und sprachähnliches Signal getestet. Co-Authored-By: Claude Opus 5 (1M context) --- provision/provision.sh | 16 ++- spike/02-s2-mikrofon.sh | 218 ++++++++++++++++++++++++++-------------- 2 files changed, 160 insertions(+), 74 deletions(-) diff --git a/provision/provision.sh b/provision/provision.sh index 859b21b..3c5525d 100755 --- a/provision/provision.sh +++ b/provision/provision.sh @@ -214,6 +214,19 @@ done # ── Berechtigungen ─────────────────────────────────────────────────────────── kopf "Berechtigungen" +# Termux selbst braucht RECORD_AUDIO und Speicherzugriff — nicht nur Termux:API. +# 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 nichts: das +# Modul lädt dann zwar, liefert aber keinen brauchbaren Ton. +for perm in \ + android.permission.RECORD_AUDIO \ + android.permission.READ_EXTERNAL_STORAGE \ + android.permission.WRITE_EXTERNAL_STORAGE +do + adbs "${perm##*.} für Termux" pm grant "$TERMUX" "$perm" 2>/dev/null || true +done + +# Termux:API bedient Kamera, Sensoren, Standort und Taschenlampe. for perm in \ android.permission.RECORD_AUDIO \ android.permission.CAMERA \ @@ -221,9 +234,10 @@ for perm in \ android.permission.READ_EXTERNAL_STORAGE \ android.permission.WRITE_EXTERNAL_STORAGE do - adbs "$(basename "${perm//./\/}") für Termux:API" pm grant com.termux.api "$perm" 2>/dev/null || true + adbs "${perm##*.} für Termux:API" pm grant com.termux.api "$perm" 2>/dev/null || true done hinweis "Fehlschläge sind normal, solange Termux:API noch nicht installiert ist." +hinweis "Eine App, die eine Berechtigung gar nicht anmeldet, lässt sich nicht beschenken." # ── Was ADB nicht kann ─────────────────────────────────────────────────────── kopf "Von Hand zu erledigen" diff --git a/spike/02-s2-mikrofon.sh b/spike/02-s2-mikrofon.sh index cc965a8..5bd8da0 100755 --- a/spike/02-s2-mikrofon.sh +++ b/spike/02-s2-mikrofon.sh @@ -3,8 +3,13 @@ # # Der stillste K.-o.-Punkt des Projekts. Ohne Mikrofon im Linux gibt es keine # Videocalls in Element oder im Browser, keine Sprachaufnahme, keine Spracheingabe. -# Die Wette: Termux' PulseAudio kann über OpenSL ES eine Aufnahmequelle anbieten, -# die per TCP in den proot durchgereicht wird. +# +# 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 @@ -13,44 +18,58 @@ require_termux DISTRO=debian DAUER="${HPOS_DAUER:-6}" -WAV="$OUT_DIR/s2-aufnahme.wav" +WAV_NAME="s2-aufnahme.wav" +WAV="$OUT_DIR/$WAV_NAME" log "HPOS Phase 0 · S2 — Mikrofon im Linux" log "Protokoll: $LOG" # ── Berechtigung ───────────────────────────────────────────────────────────── -# Ohne erteilte Mikrofonberechtigung liefert die OpenSL-Quelle Stille statt eines -# Fehlers — der verwirrendste denkbare Fehlschlag. Deshalb vorher erzwingen. -section "Mikrofonberechtigung" +# 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" +if RECHTE=$(dumpsys package com.termux 2>/dev/null | grep -i 'RECORD_AUDIO'); 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 + warn "Konnte den Berechtigungsstatus nicht auslesen" +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 -# module-sles-source ist der Teil, der wackelt: er existiert nicht in jedem -# Termux-Build. Deshalb erst mit, dann ohne — und der Unterschied wird protokolliert. -MIT_QUELLE=0 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 - MIT_QUELLE=1 ok "PulseAudio mit module-sles-source gestartet" else warn "module-sles-source ließ sich nicht laden — starte ohne" @@ -59,66 +78,92 @@ else fi sleep 2 -section "Verfügbare Quellen" +section "Quellen und Standardgeräte" if have pactl; then - pactl list sources short 2>&1 | tee -a "$LOG" | sed 's/^/ /' - QUELLEN=$(pactl list sources short 2>/dev/null | wc -l) - if [ "$QUELLEN" -gt 0 ]; then - ok "$QUELLEN Aufnahmequelle(n) gemeldet" - else - fail "Keine einzige Aufnahmequelle — S2 wird so nicht gelingen" - hint "Falls module-sles-source fehlt, ist dieser Termux-Build ohne OpenSL-Aufnahme gebaut." - fi -else - warn "pactl fehlt, kann Quellen nicht auflisten" + 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 -# ── Aufnahme im Debian ─────────────────────────────────────────────────────── -section "Aufnahme aus dem Debian heraus" -log " ${DAUER} Sekunden werden aufgenommen — bitte jetzt laut und deutlich sprechen." -log "" +# ── 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 &1 | sed 's/^/ /' || echo ' (pactl lieferte nichts)' - echo ' Nehme auf …' - timeout $((DAUER + 4)) parecord --channels=1 --rate=16000 --file-format=wav \ - /hpos/spike/out/$(basename "$WAV") & - REC=\$! - sleep $DAUER - kill \$REC 2>/dev/null - wait \$REC 2>/dev/null - echo ' Aufnahme beendet.' -" 2>&1 | tee -a "$LOG" + + # Ausdrücklich die Mikrofonquelle wählen. Ohne -d nimmt parecord die + # Standardquelle, und das ist bei geladenem Sink-Modul häufig dessen Monitor. + QUELLE=$(pactl list sources short 2>/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 ─────────────────────────────────────────────────────────────── -# Eine Datei zu erzeugen beweist nichts: die häufigste Fehlerform ist eine -# formal korrekte WAV-Datei voller Stille. Deshalb Größe UND Inhalt prüfen. section "Auswertung" if [ ! -s "$WAV" ]; then fail "Keine Aufnahmedatei entstanden → S2 gescheitert" else - GROESSE=$(stat -c%s "$WAV" 2>/dev/null || echo 0) - ok "Datei vorhanden: $WAV ($GROESSE Bytes)" - ERWARTET=$((DAUER * 16000 * 2 * 8 / 10)) - if [ "$GROESSE" -lt "$ERWARTET" ]; then - warn "Deutlich kleiner als erwartet (~$ERWARTET Bytes) — vermutlich abgebrochen" - fi - # Stilletest. Der typische Fehlschlag ist eine formal korrekte WAV-Datei voller - # Nullen — sie sieht in jeder Dateiliste richtig aus und enthält doch nichts. - # Deshalb wird der Pegel gemessen, nicht nur die Größe. + 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, und Termux -# liegt regelmäßig auf der aktuellen Version. +# 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)) @@ -128,27 +173,54 @@ try: 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}") - if spitze < 40: - print(" ✗ Praktisch Stille — die Quelle liefert keine echten Daten.") - print(" Das ist der typische Fehlschlag: Datei ja, Ton nein.") - print(" Prüfen: Mikrofonberechtigung für Termux:API, und ob") - print(" module-sles-source in diesem Termux-Build überhaupt existiert.") + + # 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(" ✓ Echtes Signal aufgenommen.") + print(" ✓ Signal vorhanden.") except Exception as e: print(f" ! Datei nicht auswertbar: {e}") PY fi - log "" - log " Zum Gegenhören: pactl upload-sample nicht nötig — einfach abspielen:" - log " proot-distro login $DISTRO -- env PULSE_SERVER=127.0.0.1 paplay /hpos/spike/out/$(basename "$WAV")" + + # ── 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 " module-sles-source geladen: $([ "$MIT_QUELLE" = 1 ] && echo ja || echo NEIN)" +log " Berechtigung com.termux: $BERECHTIGT" +log " Ton vorher gehört: ${TON_GEHOERT:-n}" -verdict "S2" "Ist auf der Aufnahme deine Stimme hörbar? - Abspielen mit dem Befehl oben. Eine Datei ohne Ton zählt als gescheitert." +verdict "S2" "War auf der Wiedergabe deine Stimme zu hören? + Nur das zählt. Ein Messwert ohne hörbare Sprache ist ein Fehlschlag."