S2: RECORD_AUDIO für Termux selbst, Quelle explizit, Diagnose dreistufig

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) <noreply@anthropic.com>
This commit is contained in:
Stefan
2026-08-09 20:23:21 +02:00
co-authored by Claude Opus 5
parent eeab61a0a4
commit f08399ba1d
2 changed files with 160 additions and 74 deletions
+15 -1
View File
@@ -214,6 +214,19 @@ done
# ── Berechtigungen ─────────────────────────────────────────────────────────── # ── Berechtigungen ───────────────────────────────────────────────────────────
kopf "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 \ for perm in \
android.permission.RECORD_AUDIO \ android.permission.RECORD_AUDIO \
android.permission.CAMERA \ android.permission.CAMERA \
@@ -221,9 +234,10 @@ for perm in \
android.permission.READ_EXTERNAL_STORAGE \ android.permission.READ_EXTERNAL_STORAGE \
android.permission.WRITE_EXTERNAL_STORAGE android.permission.WRITE_EXTERNAL_STORAGE
do 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 done
hinweis "Fehlschläge sind normal, solange Termux:API noch nicht installiert ist." 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 ─────────────────────────────────────────────────────── # ── Was ADB nicht kann ───────────────────────────────────────────────────────
kopf "Von Hand zu erledigen" kopf "Von Hand zu erledigen"
+145 -73
View File
@@ -3,8 +3,13 @@
# #
# Der stillste K.-o.-Punkt des Projekts. Ohne Mikrofon im Linux gibt es keine # Der stillste K.-o.-Punkt des Projekts. Ohne Mikrofon im Linux gibt es keine
# Videocalls in Element oder im Browser, keine Sprachaufnahme, keine Spracheingabe. # 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 # Ausführen IN TERMUX: bash spike/02-s2-mikrofon.sh
set -uo pipefail set -uo pipefail
@@ -13,44 +18,58 @@ require_termux
DISTRO=debian DISTRO=debian
DAUER="${HPOS_DAUER:-6}" 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 "HPOS Phase 0 · S2 — Mikrofon im Linux"
log "Protokoll: $LOG" log "Protokoll: $LOG"
# ── Berechtigung ───────────────────────────────────────────────────────────── # ── Berechtigung ─────────────────────────────────────────────────────────────
# Ohne erteilte Mikrofonberechtigung liefert die OpenSL-Quelle Stille statt eines # Der häufigste Grund für eine formal korrekte, aber stumme Aufnahme: PulseAudio
# Fehlers — der verwirrendste denkbare Fehlschlag. Deshalb vorher erzwingen. # läuft IM Termux-Prozess und öffnet das Mikrofon über OpenSL ES von dort aus.
section "Mikrofonberechtigung" # 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/tty || WEITER=n
case "${WEITER:-n}" in j|J) : ;; *) exit 1 ;; esac
fi
# Zusätzlich Termux:API antesten — das löst nebenbei den Systemdialog aus.
if have termux-microphone-record; then if have termux-microphone-record; then
log " Kurzer Test über Termux:API, um den Berechtigungsdialog auszulösen …"
try termux-microphone-record -d -l 2 -f "$OUT_DIR/s2-vortest.m4a" try termux-microphone-record -d -l 2 -f "$OUT_DIR/s2-vortest.m4a"
sleep 3 sleep 3
try termux-microphone-record -q try termux-microphone-record -q
if [ -s "$OUT_DIR/s2-vortest.m4a" ]; then [ -s "$OUT_DIR/s2-vortest.m4a" ] && ok "Termux:API kann aufnehmen" \
ok "Termux:API kann aufnehmen — Berechtigung liegt vor" || warn "Termux:API konnte nicht aufnehmen"
else
fail "Termux:API konnte nicht aufnehmen"
hint "Einstellungen → Apps → Termux:API → Berechtigungen → Mikrofon erlauben."
hint "Ohne das ist S2 nicht aussagekräftig."
fi
else
warn "termux-microphone-record fehlt (Paket termux-api). Weiter ohne Vortest."
fi fi
# ── PulseAudio mit Aufnahmequelle ──────────────────────────────────────────── # ── PulseAudio ───────────────────────────────────────────────────────────────
section "PulseAudio mit OpenSL-Quelle" section "PulseAudio mit OpenSL-Quelle"
pulseaudio --kill 2>/dev/null || true pulseaudio --kill 2>/dev/null || true
sleep 1 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 \ if pulseaudio --start --exit-idle-time=-1 \
--load="module-sles-source" \ --load="module-sles-source" \
--load="module-native-protocol-tcp auth-ip-acl=127.0.0.1 auth-anonymous=1" \ --load="module-native-protocol-tcp auth-ip-acl=127.0.0.1 auth-anonymous=1" \
>>"$LOG" 2>&1; then >>"$LOG" 2>&1; then
MIT_QUELLE=1
ok "PulseAudio mit module-sles-source gestartet" ok "PulseAudio mit module-sles-source gestartet"
else else
warn "module-sles-source ließ sich nicht laden — starte ohne" warn "module-sles-source ließ sich nicht laden — starte ohne"
@@ -59,66 +78,92 @@ else
fi fi
sleep 2 sleep 2
section "Verfügbare Quellen" section "Quellen und Standardgeräte"
if have pactl; then if have pactl; then
pactl list sources short 2>&1 | tee -a "$LOG" | sed 's/^/ /' pactl info 2>&1 | grep -Ei 'Default (Source|Sink)' | tee -a "$LOG" | sed 's/^/ /'
QUELLEN=$(pactl list sources short 2>/dev/null | wc -l) log " Quellen:"
if [ "$QUELLEN" -gt 0 ]; then pactl list sources short 2>&1 | tee -a "$LOG" | sed 's/^/ /'
ok "$QUELLEN Aufnahmequelle(n) gemeldet" # Die Standardquelle ist bei zwei Einträgen gern der Sink-Monitor. Der nimmt
else # auf, was ABGESPIELT wird, nicht was am Mikrofon anliegt — eine Aufnahme
fail "Keine einzige Aufnahmequelle — S2 wird so nicht gelingen" # daraus sieht technisch einwandfrei aus und enthält doch nie eine Stimme.
hint "Falls module-sles-source fehlt, ist dieser Termux-Build ohne OpenSL-Aufnahme gebaut." STANDARD=$(pactl info 2>/dev/null | awk -F': ' '/Default Source/{print $2}')
fi case "$STANDARD" in
else *monitor*) warn "Standardquelle ist ein Monitor ($STANDARD) — deshalb wird gleich"
warn "pactl fehlt, kann Quellen nicht auflisten" hint "ausdrücklich OpenSL_ES_source gewählt statt der Vorgabe." ;;
esac
fi fi
# ── Aufnahme im Debian ─────────────────────────────────────────────────────── # ── Stufe 1: Wiedergabe ──────────────────────────────────────────────────────
section "Aufnahme aus dem Debian heraus" section "Stufe 1 — kommt Ton heraus?"
log " ${DAUER} Sekunden werden aufgenommen — bitte jetzt laut und deutlich sprechen." log " Gleich sollte ein kurzer Ton zu hören sein. Lautstärke aufdrehen."
log "" 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/tty || TON_GEHOERT=n
case "${TON_GEHOERT:-n}" in
j|J) ok "Wiedergabe funktioniert — ein späteres Schweigen liegt dann an der Aufnahme." ;;
*) warn "Keine Wiedergabe. Dann sagt der Gegenhör-Test unten nichts aus:"
hint "eine gute Aufnahme wäre trotzdem nicht zu hören." ;;
esac
# ── Stufe 2: Aufnahme ────────────────────────────────────────────────────────
section "Stufe 2 — Aufnahme"
log " ${DAUER} Sekunden — bitte jetzt laut und deutlich sprechen."
sleep 1 sleep 1
proot-distro login "$DISTRO" --shared-tmp --bind "$REPO_ROOT:/hpos" -- bash -lc " HPOS_DAUER="$DAUER" HPOS_WAV="/hpos/spike/out/$WAV_NAME" \
proot-distro login "$DISTRO" --shared-tmp --bind "$REPO_ROOT:/hpos" -- bash -lc '
set -uo pipefail set -uo pipefail
export PULSE_SERVER=127.0.0.1 export PULSE_SERVER=127.0.0.1
echo ' Quellen aus Sicht des Debian:'
pactl list sources short 2>&1 | sed 's/^/ /' || echo ' (pactl lieferte nichts)' # Ausdrücklich die Mikrofonquelle wählen. Ohne -d nimmt parecord die
echo ' Nehme auf …' # Standardquelle, und das ist bei geladenem Sink-Modul häufig dessen Monitor.
timeout $((DAUER + 4)) parecord --channels=1 --rate=16000 --file-format=wav \ QUELLE=$(pactl list sources short 2>/dev/null | awk "/OpenSL_ES_source/ {print \$2; exit}")
/hpos/spike/out/$(basename "$WAV") & if [ -z "$QUELLE" ]; then
REC=\$! QUELLE=$(pactl list sources short 2>/dev/null | awk "!/monitor/ {print \$2; exit}")
sleep $DAUER fi
kill \$REC 2>/dev/null if [ -n "$QUELLE" ]; then
wait \$REC 2>/dev/null echo " Aufnahmequelle: $QUELLE"
echo ' Aufnahme beendet.' ARG="-d $QUELLE"
" 2>&1 | tee -a "$LOG" 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 ─────────────────────────────────────────────────────────────── # ── 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" section "Auswertung"
if [ ! -s "$WAV" ]; then if [ ! -s "$WAV" ]; then
fail "Keine Aufnahmedatei entstanden → S2 gescheitert" fail "Keine Aufnahmedatei entstanden → S2 gescheitert"
else else
GROESSE=$(stat -c%s "$WAV" 2>/dev/null || echo 0) ok "Datei: $WAV ($(stat -c%s "$WAV" 2>/dev/null || echo 0) Bytes)"
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.
if have python; then if have python; then
python - "$WAV" <<'PY' 2>&1 | tee -a "$LOG" python - "$WAV" <<'PY' 2>&1 | tee -a "$LOG"
import array, math, sys, wave import array, math, sys, wave
# Bewusst ohne audioop: das Modul ist in Python 3.13 entfernt worden, und Termux # Bewusst ohne audioop: das Modul ist in Python 3.13 entfernt worden.
# liegt regelmäßig auf der aktuellen Version.
try: try:
with wave.open(sys.argv[1]) as w: with wave.open(sys.argv[1]) as w:
breite = w.getsampwidth() breite = w.getsampwidth()
rate = w.getframerate()
roh = w.readframes(w.getnframes()) roh = w.readframes(w.getnframes())
if breite == 1: if breite == 1:
werte = array.array("b", bytes(b - 128 for b in roh)) 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") raise ValueError(f"{breite} Byte je Abtastwert werden hier nicht ausgewertet")
if not werte: if not werte:
raise ValueError("keine Abtastwerte enthalten") raise ValueError("keine Abtastwerte enthalten")
pegel = int(math.sqrt(sum(v * v for v in werte) / len(werte))) pegel = int(math.sqrt(sum(v * v for v in werte) / len(werte)))
spitze = max(abs(v) for v in werte) spitze = max(abs(v) for v in werte)
print(f" Effektivpegel: {pegel} Spitze: {spitze}") print(f" Effektivpegel: {pegel} Spitze: {spitze}")
if spitze < 40:
print(" ✗ Praktisch Stille — die Quelle liefert keine echten Daten.") # Sprache schwankt stark: laute Silben, Pausen dazwischen. Ein konstanter
print(" Das ist der typische Fehlschlag: Datei ja, Ton nein.") # Pegel deutet auf Brummen, Rauschen oder ein Störsignal hin — technisch ein
print(" Prüfen: Mikrofonberechtigung für Termux:API, und ob") # Signal, aber keine Aufnahme. Deshalb wird die Schwankung mitgemessen.
print(" module-sles-source in diesem Termux-Build überhaupt existiert.") 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: else:
print(" ✓ Echtes Signal aufgenommen.") print(" ✓ Signal vorhanden.")
except Exception as e: except Exception as e:
print(f" ! Datei nicht auswertbar: {e}") print(f" ! Datei nicht auswertbar: {e}")
PY PY
fi fi
log ""
log " Zum Gegenhören: pactl upload-sample nicht nötig — einfach abspielen:" # ── Stufe 3: Gegenhören ─────────────────────────────────────────────────
log " proot-distro login $DISTRO -- env PULSE_SERVER=127.0.0.1 paplay /hpos/spike/out/$(basename "$WAV")" # 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 fi
log "" 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? verdict "S2" "War auf der Wiedergabe deine Stimme zu hören?
Abspielen mit dem Befehl oben. Eine Datei ohne Ton zählt als gescheitert." Nur das zählt. Ein Messwert ohne hörbare Sprache ist ein Fehlschlag."