From 16afd5a8884a381266af869dd6a2bfc1846409d7 Mon Sep 17 00:00:00 2001 From: Victor Rubens Date: Fri, 21 Aug 2026 13:03:40 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Decode=208-bit,=2016-bit?= =?UTF-8?q?=20and=20float=20PCM=20samples=20correctly=20on=20Android?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three PCM paths in WaveformExtractor mis-read the decoder output, so extracted waveforms did not match the audio. - 8-bit: ENCODING_PCM_8BIT is unsigned with 128 as silence, but the byte was read as a signed Kotlin Byte, so silence decoded as -1.0. - 16-bit: the low byte was not masked, so any byte above 0x7F widened to a negative Int whose sign bits swallowed the high byte through the `or`. Full-scale +32767 decoded as -0.00003. - 32-bit: a pcmEncodingBit of 32 is only set for ENCODING_PCM_FLOAT, but the bytes were assembled as an integer and divided by 2^31. Both +1.0 and -1.0 decoded as -0.0039. --- .../audio_waveforms/WaveformExtractor.kt | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/android/src/main/kotlin/com/simform/audio_waveforms/WaveformExtractor.kt b/android/src/main/kotlin/com/simform/audio_waveforms/WaveformExtractor.kt index 46201a91..287b9aec 100644 --- a/android/src/main/kotlin/com/simform/audio_waveforms/WaveformExtractor.kt +++ b/android/src/main/kotlin/com/simform/audio_waveforms/WaveformExtractor.kt @@ -9,6 +9,7 @@ import android.os.Build import android.util.Log import io.flutter.plugin.common.MethodChannel import java.nio.ByteBuffer +import java.nio.ByteOrder import java.util.concurrent.CountDownLatch import kotlin.math.pow import kotlin.math.sqrt @@ -299,7 +300,10 @@ class WaveformExtractor( */ private fun handle8bit(size: Int, buf: ByteBuffer) { repeat(size / if (channels == 2) 2 else 1) { - val result = buf.get().toInt() / Constants.EIGHT_BITS + // ENCODING_PCM_8BIT is unsigned, with 128 as silence, while a Kotlin + // Byte is signed: it has to be widened and then centered on zero. + val sample = (buf.get().toInt() and 0xFF) - 128 + val result = sample / Constants.EIGHT_BITS if (channels == 2) { buf.get() } @@ -318,9 +322,13 @@ class WaveformExtractor( */ private fun handle16bit(size: Int, buf: ByteBuffer) { repeat(size / if (channels == 2) 4 else 2) { - val first = buf.get().toInt() - val second = buf.get().toInt() shl 8 - val value = (first or second) / Constants.SIXTEEN_BITS + // Little endian, low byte first. The low byte has to be masked: a + // Kotlin Byte above 0x7F widens to a negative Int, and its sign bits + // then swallow the high byte through the `or`. The high byte is left + // signed on purpose — that is the sample's own sign. + val low = buf.get().toInt() and 0xFF + val high = buf.get().toInt() shl 8 + val value = (low or high) / Constants.SIXTEEN_BITS if (channels == 2) { buf.get() buf.get() @@ -330,26 +338,21 @@ class WaveformExtractor( } /** - * Processes 32-bit PCM audio data + * Processes 32-bit float PCM audio data + * + * A pcmEncodingBit of 32 is only ever set for AudioFormat.ENCODING_PCM_FLOAT, + * so the samples are IEEE-754 floats already in the range [-1.0, 1.0] and are + * read as such, then passed to handleBufferDivision for RMS calculation. * - * Reads 32-bit samples from the buffer, normalizes them to the range [-1.0, 1.0], - * and passes them to handleBufferDivision for RMS calculation. - * * @param size Size of the buffer in bytes * @param buf ByteBuffer containing the audio data */ private fun handle32bit(size: Int, buf: ByteBuffer) { + val floats = buf.order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer() repeat(size / if (channels == 2) 8 else 4) { - val first = buf.get().toLong() - val second = buf.get().toLong() shl 8 - val third = buf.get().toLong() shl 16 - val forth = buf.get().toLong() shl 24 - val value = (first or second or third or forth) / Constants.THIRTY_TWO_BITS + val value = floats.get() if (channels == 2) { - buf.get() - buf.get() - buf.get() - buf.get() + floats.get() } handleBufferDivision(value) }