From 81d6d0ed2003b4507db293fc93b395a8f92e846d Mon Sep 17 00:00:00 2001 From: Darren Ng Date: Sun, 27 Sep 2026 21:52:52 -0700 Subject: [PATCH] fix(android): read volume samples in native byte order useTrackVolume's Android VolumeProcessor read 16-bit PCM from WebRTC's AudioTrackSink buffer with getShort() without setting a byte order. The buffer is a JNI direct buffer, which Java treats as big-endian, while the samples are little-endian, so every sample was byte-swapped and any non-trivial audio read as roughly -5 dBFS. Read through a duplicate() view in native order, which also leaves the sink's buffer position and order untouched. Co-authored-by: Cursor --- .changeset/android-volume-byte-order.md | 5 +++++ .../audio/processing/VolumeProcessor.kt | 15 +++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 .changeset/android-volume-byte-order.md diff --git a/.changeset/android-volume-byte-order.md b/.changeset/android-volume-byte-order.md new file mode 100644 index 0000000..942f7e2 --- /dev/null +++ b/.changeset/android-volume-byte-order.md @@ -0,0 +1,5 @@ +--- +'@livekit/react-native': patch +--- + +android: Fix useTrackVolume reading audio samples in the wrong byte order diff --git a/android/src/main/java/com/livekit/reactnative/audio/processing/VolumeProcessor.kt b/android/src/main/java/com/livekit/reactnative/audio/processing/VolumeProcessor.kt index df73fe6..4663850 100644 --- a/android/src/main/java/com/livekit/reactnative/audio/processing/VolumeProcessor.kt +++ b/android/src/main/java/com/livekit/reactnative/audio/processing/VolumeProcessor.kt @@ -6,6 +6,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule import com.livekit.reactnative.audio.events.Events import livekit.org.webrtc.AudioTrackSink import java.nio.ByteBuffer +import java.nio.ByteOrder import kotlin.math.round import kotlin.math.sqrt @@ -34,17 +35,20 @@ abstract class BaseVolumeProcessor : AudioTrackSink { numberOfFrames: Int, absoluteCaptureTimestampMs: Long ) { - audioData.mark() - audioData.position(0) + // WebRTC hands us a JNI direct buffer, which Java defaults to big-endian, + // while the PCM samples are native (little-endian). Read through a view + // in native order so the sink's buffer is left untouched. + val samples = audioData.duplicate().order(ByteOrder.nativeOrder()) + samples.position(0) var average = 0L val bytesPerSample = bitsPerSample / 8 // RMS average calculation for (i in 0 until numberOfFrames) { val value = when (bytesPerSample) { - 1 -> audioData.get().toLong() - 2 -> audioData.getShort().toLong() - 4 -> audioData.getInt().toLong() + 1 -> samples.get().toLong() + 2 -> samples.getShort().toLong() + 4 -> samples.getInt().toLong() else -> throw IllegalArgumentException() } @@ -60,7 +64,6 @@ abstract class BaseVolumeProcessor : AudioTrackSink { 4 -> volume / Int.MAX_VALUE else -> throw IllegalArgumentException() } - audioData.reset() onVolumeCalculated(volumeNormalized) }