Skip to content

fix: 🐛 Decode 8-bit, 16-bit and float PCM samples correctly on Android - #513

Open
victorrss wants to merge 1 commit into
SimformSolutionsPvtLtd:mainfrom
nubank:fix/android-pcm-sample-decoding
Open

fix: 🐛 Decode 8-bit, 16-bit and float PCM samples correctly on Android#513
victorrss wants to merge 1 commit into
SimformSolutionsPvtLtd:mainfrom
nubank:fix/android-pcm-sample-decoding

Conversation

@victorrss

Copy link
Copy Markdown

Description

WaveformExtractor mis-reads the decoder's PCM output in all three of its bit-depth paths, so the extracted waveform does not match the audio. Nothing crashes — the waveform simply has the wrong shape — which is why this has been easy to miss. Full analysis and a value-by-value comparison are in #512.

16-bit (handle16bit), the default and most common path:

val first = buf.get().toInt()          // signed: 0x80..0xFF widen to negative
val second = buf.get().toInt() shl 8
val value = (first or second) / Constants.SIXTEEN_BITS

buf.get() returns a signed Byte, so a low byte above 0x7F widens to a negative Int whose sign bits then swallow the high byte through the or. Full-scale +32767 decodes as -0.00003. Since about half of all samples have a low byte ≥ 0x80, roughly half collapse toward zero, making the waveform attenuated and noisy rather than visibly broken. Fixed by masking the low byte with and 0xFF; the high byte stays signed, because that is the sample's own sign.

8-bit (handle8bit): AudioFormat.ENCODING_PCM_8BIT is unsigned with 128 as silence, but the byte was read as a signed Byte and never centered, so silence decoded as full-scale negative. Fixed by widening with and 0xFF and subtracting 128.

32-bit (handle32bit): pcmEncodingBit is only ever set to 32 for AudioFormat.ENCODING_PCM_FLOAT (in onOutputFormatChanged), so these samples are IEEE-754 floats already in [-1.0, 1.0]. They were assembled byte-by-byte as an integer and divided by 2^31, which made +1.0 and -1.0 both decode as -0.0039. Fixed by reading them as floats via asFloatBuffer(), which also simplifies the stereo channel skip.

No public API changes, and no behavioral change beyond the decoded values being correct.

Checklist

  • The title of my PR starts with a Conventional Commit prefix (fix:, feat:, docs: etc).
  • I have followed the Contributor Guide when preparing my PR.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • I have updated/added relevant examples in examples or docs.

On tests: the Android source set has no test harness today (android/src/ contains only main, and there are no JUnit dependencies or testOptions in android/build.gradle), and these three functions are private and read instance state, so there is nowhere to hang a unit test without first introducing the JVM test setup. I did not want to bundle that decision into a bug fix. The issue instead documents the exact input/output pairs for each path, and I am happy to add a proper test — extracting the arithmetic into testable helpers, plus the JUnit wiring — if you would like it in this PR or a follow-up.

Docs and examples are not applicable: this is an internal native decoding change with no surface in the Dart API.

Verified with ./gradlew :audio_waveforms:compileDebugKotlin (clean; the two remaining warnings are pre-existing, in AudioPlayer.kt) and by decoding known sample values through both the old and the new arithmetic.

Breaking Change?

  • Yes, this PR is a breaking change.
  • No, this PR is not a breaking change.

Waveforms extracted on Android will change shape, but only to become correct — they will now match what iOS produces for the same file.

Related Issues

Fixes #512

Made with Cursor

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android: extracted waveform does not match the audio — 8-bit, 16-bit and float PCM samples are decoded incorrectly

1 participant