From: Helpdesk 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. + * + *

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);