Skip to content

Commit 64423c7

Browse files
committed
feat: add dual-track recording to capture each side of a call separately
Adds a "Record each side separately" toggle that captures the uplink (your mic) and downlink (the other party) as two independent, time-synced files instead of one mixed recording, so each speaker can be clearly attributed for transcription.
1 parent 3cdb19d commit 64423c7

12 files changed

Lines changed: 366 additions & 149 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ It can also be seen as an on-device wrapper for [scrcpy-server](https://github.c
2626
- Ignore specific contacts
2727
- Ignore all contacts
2828
- Saves recordings with **Opus** or **AAC** codec.
29+
- Optionally records each side of the call (uplink/downlink) as two separate, time-synced files, useful for transcription with clear speaker separation.
2930
- The app runs only on phone event changes, no persistent background process and notifications
3031

3132
## Requirements

‎app/src/main/aidl/com/kitsumed/shizucallrecorder/IShellService.aidl‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,31 @@ interface IShellService {
4040
*/
4141
void stopRecording() = 3;
4242

43+
/**
44+
* Starts a second, independent audio-capture pipeline in parallel with [startRecording].
45+
* Used for dual-track recording, where each side of a call (uplink/downlink) is captured
46+
* as its own scrcpy-server process and its own pipe, so the two streams never mix.
47+
*
48+
* @param audioSource scrcpy audio_source parameter (e.g. "voice-call-downlink").
49+
* @param audioCodec scrcpy audio_codec parameter (e.g. "opus", "aac").
50+
* @param audioBitRate scrcpy audio_bit_rate in bps (e.g. 16000 for 16 kbps Opus).
51+
* @param serverPath Absolute path to scrcpy-server.jar in shared storage.
52+
* @param isDebuggingModeEnabled When true, logs relay throughput every second.
53+
* @return The read-end [ParcelFileDescriptor] of the audio pipe, or null on failure.
54+
*/
55+
ParcelFileDescriptor startSecondaryRecording(
56+
String audioSource,
57+
String audioCodec,
58+
int audioBitRate,
59+
String serverPath,
60+
boolean isDebuggingModeEnabled
61+
) = 7;
62+
63+
/**
64+
* Stops the secondary audio capture pipeline started by [startSecondaryRecording].
65+
*/
66+
void stopSecondaryRecording() = 8;
67+
4368
/**
4469
* Grants an AppOp permission at the package level for a specific user profile.
4570
* @param packageName The package name of the app to grant the permission to.

‎app/src/main/java/com/kitsumed/shizucallrecorder/data/AppPreferences.kt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class AppPreferences(context: Context) {
4545
const val VIBRATION_ENABLED = true
4646
val CALL_DETECTION_MODE = CallDetectionMode.getDefaultModeForDevice().key
4747
const val RECORD_THIRD_PARTY_CALLS = false
48+
const val DUAL_TRACK_RECORDING_ENABLED = false
4849

4950
const val POST_RECORDING_FILE_ACTIONS_NOTIFICATION_ENABLED = false
5051
const val AUTO_RECORD_INCOMING = false
@@ -123,7 +124,8 @@ class AppPreferences(context: Context) {
123124
SHIZUKU_KEEP_ALIVE("shizuku_keep_alive"),
124125
SHIZUKU_AUTH_KEY("shizuku_auth_key"),
125126
CALL_DETECTION_MODE("call_detection_mode"),
126-
RECORD_THIRD_PARTY_CALLS("record_third_party_calls");
127+
RECORD_THIRD_PARTY_CALLS("record_third_party_calls"),
128+
DUAL_TRACK_RECORDING_ENABLED("dual_track_recording_enabled");
127129
}
128130

129131
// -------- Nested enums
@@ -377,6 +379,15 @@ class AppPreferences(context: Context) {
377379
/** Sets the configured audio bitrate. */
378380
fun setAudioBitRate(bitRate: Int) = setInt(Key.AUDIO_BITRATE, bitRate)
379381

382+
/**
383+
* Checks if dual-track recording is enabled. When enabled, each side of the call is captured
384+
* separately (uplink/downlink) into two files instead of one, ignoring [getAudioSource].
385+
*/
386+
fun isDualTrackRecordingEnabled() = getBoolean(Key.DUAL_TRACK_RECORDING_ENABLED, DefaultsValue.DUAL_TRACK_RECORDING_ENABLED)
387+
388+
/** Sets whether dual-track recording is enabled. */
389+
fun setDualTrackRecordingEnabled(enabled: Boolean) = setBoolean(Key.DUAL_TRACK_RECORDING_ENABLED, enabled)
390+
380391
// -------- File Naming --------
381392

382393
/** Gets the user configured file name template. */

‎app/src/main/java/com/kitsumed/shizucallrecorder/integrations/scrcpy/ScrcpyAudioMuxer.kt‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,15 @@ import java.nio.ByteBuffer
3737
*
3838
* @param outputFileDescriptor Writable [java.io.FileDescriptor] for the output file (from SAF).
3939
* @param outputDisplayPath Human-readable path for logs (e.g. "Recordings/call_….ogg").
40+
* @param sharedOriginNanos Optional [System.nanoTime] instant to use as PTS=0 instead of this
41+
* muxer's own first packet. Used for dual-track recording so both
42+
* tracks' timelines share the exact same wall-clock origin and stay
43+
* in sync when merged externally (e.g. for transcription).
4044
*/
4145
class ScrcpyAudioMuxer(
4246
private val outputFileDescriptor: java.io.FileDescriptor,
43-
private val outputDisplayPath: String
47+
private val outputDisplayPath: String,
48+
private val sharedOriginNanos: Long? = null
4449
) : Closeable {
4550

4651
// Muxer state
@@ -144,7 +149,7 @@ class ScrcpyAudioMuxer(
144149
// dead time from our internal timeline so the final output file doesn't have a huge silence.
145150
// TODO: This code was added to fix most phone audio player behaving badly with the large silence, ideally we would like to keep the silence and not corrupt the file by doing so.
146151
if (firstPacketTimeNanos == -1L) {
147-
firstPacketTimeNanos = nowNanos
152+
firstPacketTimeNanos = sharedOriginNanos ?: nowNanos
148153
lastPacketWallClockNanos = nowNanos
149154
AppLogger.d( "First audio frame: wall-clock origin set, pts=0")
150155
} else {

0 commit comments

Comments
 (0)