Prd/components/dialer-patch/patches/0010-callrecord-audio-quality.patch
Lucy Doupalů be9f14ce34 Helpdesk - operator console + patched GrapheneOS Dialer for call handling
A small helpdesk system: an office Pixel running a patched GrapheneOS Dialer
answers technician calls, records both call legs as separate channels, and a
Ruby backend transcribes them through Whisper and files an AI summary against
the caller.

Squashed to a single commit for sharing. No credentials are included; secrets
live outside the repo in /etc/helpdesk/env on the server or a gitignored
.claude/env.local locally. See .claude/env.local.example for the shape.

Start at README.md, then docs/architecture.md.
2026-07-27 18:50:32 +02:00

121 lines
7 KiB
Diff

From: Helpdesk <helpdesk@local>
Subject: [PATCH] callrecord: runtime-selectable audio quality + a probe for per-speaker capture
Stock call recording is AAC at 16000 bits/sec over a 16000 Hz stream, about one bit per sample. That is
lossy enough to corrupt ordinary Czech words before Whisper ever sees them: production transcripts
render "balíček" as "badíček" and "zmeškaný" as "myštěný". Both Whisper models the service offers make
the SAME mistakes on those words, which is what rules out the model and points at the audio.
Rather than hardcode a different number, the bitrate and output format become persist.helpdesk.rec_*
properties. Each candidate can only be judged by placing a real call and scoring the transcript, and a
rebuild-and-reflash per experiment costs hours, so one image covers the whole comparison:
adb shell setprop persist.helpdesk.rec_bitrate 96000 # better AAC
adb shell setprop persist.helpdesk.rec_format 2 # lossless WAV
adb shell setprop persist.helpdesk.rec_bitrate "" # back to stock
Defaults reproduce stock behaviour exactly, so an unprovisioned device records as it always has.
Also adds an opt-in probe (persist.helpdesk.rec_probe_sources=1) logging whether VOICE_UPLINK and
VOICE_DOWNLINK can be opened. Google Dialer records those as two AudioRecords and interleaves them into
a 2-channel WAV so caller and callee are exactly separable rather than left to a diariser to infer (see
the note in WavLPCMRecorder). Whether this device permits it is decided by the audio HAL and sepolicy,
not by the source, so it needs answering on hardware before anyone writes the dual-capture path. The
probe opens and immediately releases both and changes nothing about the recording.
Applies on top of patches 0001-0009.
---
diff --git a/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java b/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java
index a64d0fc..e0a1de6 100644
--- a/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java
+++ b/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java
@@ -107,6 +107,49 @@ public abstract class BaseCallRecorder implements Closeable {
mAudioRecord.getAudioFormat()));
Log.d(TAG, "mPcmBufferSize " + mPcmBufferSize);
mAudioBufferPool = new ByteBufferPool(BUFFER_POOL_NUM_BUFFERS, mPcmBufferSize);
+ if (com.android.dialer.helpdesk.HelpdeskConfig.recordingProbeSources()) {
+ probeSeparateCallLegs();
+ }
+ }
+
+ /**
+ * Helpdesk patch: report whether the two call legs can be captured separately on this device.
+ * Opt-in via persist.helpdesk.rec_probe_sources=1; opens and immediately releases two AudioRecords
+ * and changes nothing about the recording in progress.
+ *
+ * <p>Recording the legs as two channels (the way Google Dialer does, see the note in
+ * WavLPCMRecorder) would make caller and callee exactly separable instead of leaving a diariser to
+ * infer it. Whether those sources are capturable is decided by the audio HAL and sepolicy, so it can
+ * only be answered on hardware. STATE_INITIALIZED on both is the green light for building that path.
+ */
+ private void probeSeparateCallLegs() {
+ final int[] sources = {
+ android.media.MediaRecorder.AudioSource.VOICE_UPLINK,
+ android.media.MediaRecorder.AudioSource.VOICE_DOWNLINK,
+ };
+ final String[] names = {"VOICE_UPLINK", "VOICE_DOWNLINK"};
+ for (int i = 0; i < sources.length; i++) {
+ AudioRecord probe = null;
+ try {
+ int min = AudioRecord.getMinBufferSize(mAudioFormat.getSampleRate(),
+ AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
+ probe = new AudioRecord(sources[i], mAudioFormat.getSampleRate(),
+ AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, Math.max(min, 4096));
+ boolean ok = probe.getState() == AudioRecord.STATE_INITIALIZED;
+ Log.i(TAG, "helpdesk source probe: " + names[i] + " -> " + (ok ? "AVAILABLE" : "uninitialised"));
+ } catch (Throwable t) {
+ Log.i(TAG, "helpdesk source probe: " + names[i] + " -> REFUSED ("
+ + t.getClass().getSimpleName() + ": " + t.getMessage() + ")");
+ } finally {
+ if (probe != null) {
+ try {
+ probe.release();
+ } catch (Throwable ignored) {
+ // releasing a probe must never affect the real recording
+ }
+ }
+ }
+ }
}
protected final long computePresentationTimeUs(int bytesRead) {
diff --git a/java/com/android/dialer/callrecord/impl/CallRecorderServiceV2.java b/java/com/android/dialer/callrecord/impl/CallRecorderServiceV2.java
index 71dc86b..0eb2dfe 100644
--- a/java/com/android/dialer/callrecord/impl/CallRecorderServiceV2.java
+++ b/java/com/android/dialer/callrecord/impl/CallRecorderServiceV2.java
@@ -106,6 +106,13 @@ public class CallRecorderServiceV2 extends Service {
}
private OutputFormat getOutputFormat() {
+ // Helpdesk patch: persist.helpdesk.rec_format wins when set, so the recording format can be
+ // changed on a flashed device without a rebuild. Unset (-1) keeps the user's own preference.
+ int formatOverride = com.android.dialer.helpdesk.HelpdeskConfig.recordingFormat();
+ if (formatOverride >= 0) {
+ Log.i(TAG, "output format overridden by persist.helpdesk.rec_format=" + formatOverride);
+ return OutputFormat.getOutputFormat(formatOverride);
+ }
String def = getString(R.string.call_recording_output_format_default);
int selectionId = parseInt(getPrefs().getString(KEY_CALL_RECORDING_OUTPUT_FORMAT, def));
return OutputFormat.getOutputFormat(selectionId);
diff --git a/java/com/android/dialer/callrecord/impl/MediaCodecRecorder.java b/java/com/android/dialer/callrecord/impl/MediaCodecRecorder.java
index c3783fa..a52fb97 100644
--- a/java/com/android/dialer/callrecord/impl/MediaCodecRecorder.java
+++ b/java/com/android/dialer/callrecord/impl/MediaCodecRecorder.java
@@ -52,7 +52,16 @@ public class MediaCodecRecorder extends BaseCallRecorder {
default:
throw new IllegalArgumentException("unexpected output format " + outputFormat);
}
- mMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, outputFormat.bitRate);
+ // Helpdesk patch: persist.helpdesk.rec_bitrate overrides the format's built-in bitrate. The stock
+ // 16000 bits/sec on a 16000 Hz stream is roughly one bit per sample and mangles speech badly enough
+ // to show up downstream as transcription errors on ordinary words. 0 = unset = keep stock.
+ int bitRateOverride = com.android.dialer.helpdesk.HelpdeskConfig.recordingBitRate();
+ int bitRate = bitRateOverride > 0 ? bitRateOverride : outputFormat.bitRate;
+ if (bitRateOverride > 0) {
+ Log.i(TAG, "bitrate overridden by persist.helpdesk.rec_bitrate: "
+ + outputFormat.bitRate + " -> " + bitRate);
+ }
+ mMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
final String encoderForFormat = new MediaCodecList(MediaCodecList.REGULAR_CODECS)
.findEncoderForFormat(mMediaFormat);