From: Helpdesk Subject: [PATCH] callrecord: record the two call legs as separate channels VOICE_CALL hands back one pre-mixed stream, so the transcriber has to infer who was speaking, and a diariser guessing between two similar voices on a narrowband line gets it wrong. Captured as separate channels - left uplink (operator), right downlink (caller) - speaker attribution stops being a guess: each channel is one person by construction. AOSP's own note in WavLPCMRecorder observes that Google Dialer does exactly this and speculates it is for transcription. Opt in with persist.helpdesk.rec_dual_leg=1, ideally alongside rec_format=2 (WAV). Off by default. The interleaving lives in the overlay (helpdesk/DualLegCapture.java); this patch only teaches BaseCallRecorder that its audio source may be two AudioRecords rather than one. The source is chosen in the constructor because it decides the channel count, which the audio format, the PCM buffer size and the WAV header are all derived from. mAudioRecord and mDualLeg are mutually exclusive - exactly one is the source for any given recording. Falls back silently and completely. Whether both sources can be opened AT ONCE is decided by the audio HAL and sepolicy, and a probe that opens them one at a time cannot answer it; if either refuses, DualLegCapture.open returns null and recording proceeds on the mixed source exactly as before. Nothing here is load-bearing for capturing a call. Sizing now comes from mAudioFormat rather than mAudioRecord, since the latter does not exist in the dual-leg case; the values are identical in the single-source case. Applies on top of patches 0001-0010. --- diff --git a/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java b/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java index e0a1de6..ca6257a 100644 --- a/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java +++ b/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java @@ -54,7 +54,10 @@ public abstract class BaseCallRecorder implements Closeable { protected final OutputFormat mOutputFormat; protected final AudioFormat mAudioFormat; - private final AudioRecord mAudioRecord; + private final AudioRecord mAudioRecord; // null when recording the legs separately + // Helpdesk patch: non-null when the two call legs are captured as separate channels. Mutually + // exclusive with mAudioRecord - exactly one of the two is the audio source for a given recording. + private final com.android.dialer.helpdesk.DualLegCapture mDualLeg; protected final ContentResolver mContentResolver; private ParcelFileDescriptor mWritePfd; @@ -88,23 +91,35 @@ public abstract class BaseCallRecorder implements Closeable { mOutputFormat = outputFormat; this.mContentResolver = context.getApplicationContext().getContentResolver(); + // Helpdesk patch: decide the audio source BEFORE building the format, because capturing the legs + // separately makes the stream stereo and everything below is sized from the channel count. Opening + // is attempted here rather than at start, so a device that refuses falls back before any state is + // built on the assumption. + mDualLeg = com.android.dialer.helpdesk.HelpdeskConfig.recordingDualLeg() + ? com.android.dialer.helpdesk.DualLegCapture.open(outputFormat.sampleRate) + : null; + final int channelMask = mDualLeg != null + ? AudioFormat.CHANNEL_IN_STEREO : outputFormat.channelMask; this.mAudioFormat = new AudioFormat.Builder() - .setChannelMask(outputFormat.channelMask) + .setChannelMask(channelMask) .setSampleRate(outputFormat.sampleRate) .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .build(); // Google Dialer apparently uses mAudioFormat.getSampleRate() for the AudioRecord buffer size. final int bufferSizeInBytes = mAudioFormat.getSampleRate(); - mAudioRecord = new AudioRecord(audioSource, mAudioFormat.getSampleRate(), - mAudioFormat.getChannelMask(), mAudioFormat.getEncoding(), bufferSizeInBytes); + // Only one source exists per recording: the mixed VOICE_CALL stream, or the two legs. + mAudioRecord = mDualLeg != null ? null + : new AudioRecord(audioSource, mAudioFormat.getSampleRate(), + mAudioFormat.getChannelMask(), mAudioFormat.getEncoding(), bufferSizeInBytes); mUri = uri; - final int framesInDurationMs = mAudioRecord.getSampleRate() * DURATION_TO_READ_MS / 1000; + // Sized from mAudioFormat rather than mAudioRecord, which does not exist in the dual-leg case. + final int framesInDurationMs = mAudioFormat.getSampleRate() * DURATION_TO_READ_MS / 1000; mPcmBufferSize = Math.max( mAudioFormat.getFrameSizeInBytes() * framesInDurationMs, AudioRecord.getMinBufferSize( - mAudioRecord.getSampleRate(), mAudioRecord.getChannelConfiguration(), - mAudioRecord.getAudioFormat())); + mAudioFormat.getSampleRate(), mAudioFormat.getChannelMask(), + mAudioFormat.getEncoding())); Log.d(TAG, "mPcmBufferSize " + mPcmBufferSize); mAudioBufferPool = new ByteBufferPool(BUFFER_POOL_NUM_BUFFERS, mPcmBufferSize); if (com.android.dialer.helpdesk.HelpdeskConfig.recordingProbeSources()) { @@ -211,7 +226,15 @@ public abstract class BaseCallRecorder implements Closeable { onRecordingStart(mWritePfd); Log.d(TAG, "start recording"); - mAudioRecord.startRecording(); + // Helpdesk patch: start whichever source this recording is using. + if (mDualLeg != null) { + if (!mDualLeg.start()) { + Log.e(TAG, "dual-leg capture failed to start; aborting this recording"); + return; + } + } else { + mAudioRecord.startRecording(); + } } catch (Throwable t) { closeWritePfd(); throw t; @@ -271,7 +294,9 @@ public abstract class BaseCallRecorder implements Closeable { } private boolean shouldContinue() { - return mIsRecording && mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING; + return mIsRecording && (mDualLeg != null + ? mDualLeg.isRecording() + : mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING); } private void closeQuietly() { @@ -323,8 +348,17 @@ public abstract class BaseCallRecorder implements Closeable { } // AudioRecord does not change the ByteBuffer position - int read = mAudioRecord.read(mBuffer.data, mBuffer.data.capacity(), - AudioRecord.READ_NON_BLOCKING); + // Helpdesk patch: in dual-leg mode the read interleaves both legs into this buffer instead. It + // returns 0 until both have produced a common frame, and -1 only when both legs error, which the + // hold-resilient branch below already handles correctly. + int read; + if (mDualLeg != null) { + mBuffer.data.clear(); + read = mDualLeg.read(mBuffer.data); + } else { + read = mAudioRecord.read(mBuffer.data, mBuffer.data.capacity(), + AudioRecord.READ_NON_BLOCKING); + } if (read > 0) { mReadErrorStreak = 0; // This will set buffer's limit and reset position @@ -358,7 +392,11 @@ public abstract class BaseCallRecorder implements Closeable { private void stopAudioRecordResourcesAndClosePool() { try { - mAudioRecord.stop(); + if (mDualLeg != null) { + mDualLeg.stop(); + } else { + mAudioRecord.stop(); + } } catch (IllegalStateException ignored) { } // After the pool is closed, the consumer thread will continue until no more buffers left to @@ -487,7 +525,11 @@ public abstract class BaseCallRecorder implements Closeable { mIsRecording = false; onClose(); mAudioBufferPool.close(); - mAudioRecord.release(); + if (mDualLeg != null) { + mDualLeg.release(); + } else { + mAudioRecord.release(); + } mAudioBufferProducerExecutor.shutdownNow(); mAudioBufferConsumerExecutor.shutdownNow(); closeWritePfd();