Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Stream decoded PCM to disk during Android WAV conversion#20

Merged
sjoenk merged 9 commits intomainfrom
feature/14-implement-streaming-output-to-disk-during-decoding
Feb 14, 2026
Merged

Stream decoded PCM to disk during Android WAV conversion#20
sjoenk merged 9 commits intomainfrom
feature/14-implement-streaming-output-to-disk-during-decoding

Conversation

@sjoenk
Copy link
Owner

@sjoenk sjoenk commented Feb 14, 2026

Summary

  • Writes decoded PCM chunks directly to disk via RandomAccessFile instead of accumulating all data in a ByteArrayOutputStream before writing
  • Uses a placeholder WAV header that is updated with the actual data size after decoding completes
  • Falls back to the original buffered approach when resampling is requested, since the linear interpolation algorithm requires the full PCM buffer

Memory impact

Scenario Before After
Standard conversion (no resampling) ~2x decoded PCM size (~80 MB for a 10 MB MP3) ~buffer size (a few KB)
With resampling ~2x decoded PCM size Unchanged (buffered fallback)

Benchmark results

Tested on Android emulator (Medium_Phone, API 36, arm64-v8a, ~192 MB heap limit) with a ~10 min MP3 (16 MB compressed → ~119 MB PCM).

main branch (buffered)

Test Result
MP3 → WAV OOM CRASH
java.lang.OutOfMemoryError: Failed to allocate a 204341264 byte allocation
  with 25165824 free bytes and 92MB until OOM
    at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:120)
    at AudioDecoderPlugin.performConversion(AudioDecoderPlugin.kt:368)

The old code buffers all decoded PCM (~195 MB) in a ByteArrayOutputStream before writing to disk, exceeding the heap limit.

This branch (streaming)

Test Time Output
MP3 → WAV (default) 24.0 s 119.2 MB
M4A → WAV (default) 43.5 s 119.2 MB
MP3 → WAV (mono) 21.9 s 59.6 MB
MP3 → WAV (24-bit) 22.6 s 178.7 MB
WAV → M4A 292.8 s 10.9 MB

All 5 conversions completed successfully with no memory issues. The streaming approach writes PCM chunks directly to disk, keeping memory usage independent of file size.

Test plan

  • Run existing unit tests (flutter test)
  • Convert a large audio file to WAV on Android and verify output plays correctly
  • Convert with channel conversion (e.g. stereo to mono) and verify output
  • Convert with bit depth change (e.g. 16-bit to 24-bit) and verify output
  • Convert with resampling (e.g. 44100 to 22050) to exercise the buffered fallback path
  • Run all 23 integration tests on Android emulator

Closes #14

Instead of accumulating all decoded PCM data in a ByteArrayOutputStream
before writing, use a RandomAccessFile to write each decoded chunk
directly to disk. A placeholder WAV header is written first and updated
with the actual data size after decoding completes.

This reduces peak memory usage from O(file_size) to O(buffer_size) for
the common case (no resampling). When resampling is requested, the
original buffered approach is used as a fallback since the linear
interpolation algorithm requires the full PCM buffer.

Closes #14
@sjoenk sjoenk linked an issue Feb 14, 2026 that may be closed by this pull request
- Add AudioTrackInfo data class and extractAudioTrack() to deduplicate
  track discovery logic between performConversion and performConversionBuffered
- Wrap streaming path in try/catch/finally to delete partial output files
  on failure and guarantee codec/extractor release
- Document Int/WAV 2 GB size field limitation on totalPcmBytes
- Restore clarifying comments in performConversionBuffered
- Reuse existing mime variable in extractAudioTrack instead of re-fetching
- Pass AudioTrackInfo directly to performConversionBuffered to avoid
  double extraction when resampling is requested
- Add try/catch/finally to performConversionBuffered for consistent
  error handling and resource cleanup
- Replace mutable chunk variable with idiomatic let-chain
- Replace outdated getPlatformVersion test with argument validation
  tests covering all method channel endpoints
Use Long for totalPcmBytes and validate against the WAV format's
~4 GB data limit (uint32 RIFF size field) so that oversized files
produce a clear error instead of a corrupt header.
Prevents stale bytes from a previous larger file remaining at the
end of the output when RandomAccessFile opens in read-write mode.
Cover channel conversion, bit depth conversion, and combined
channel + bit depth without resampling. Verify that the WAV header
data and RIFF chunk sizes match the actual PCM payload.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements streaming PCM output to disk during Android WAV conversion to reduce memory usage from O(file_size) to O(buffer_size). The implementation writes decoded PCM chunks directly to disk via RandomAccessFile with a placeholder WAV header that is updated after decoding completes. When resampling is requested, the code falls back to the original buffered approach since the linear interpolation algorithm requires the full PCM buffer.

Changes:

  • Introduced streaming WAV conversion that writes PCM data incrementally to disk instead of buffering in memory
  • Added fallback to buffered conversion when resampling is needed
  • Added comprehensive integration tests for the streaming path and unit tests for error handling

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
android/src/main/kotlin/nl/silversoft/audio_decoder/AudioDecoderPlugin.kt Implements streaming PCM output with RandomAccessFile, adds extractAudioTrack helper, splits conversion into streaming and buffered paths, adds MAX_WAV_DATA_SIZE validation
example/integration_test/plugin_integration_test.dart Adds tests for streaming path with channel/bit-depth conversion and WAV header validation
android/src/test/kotlin/nl/silversoft/audio_decoder/AudioDecoderPluginTest.kt Adds comprehensive unit tests for argument validation across all method calls, removes outdated comments

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Move codec.configure/start inside try block so failures are caught by
the finally clause. Wrap codec.stop() in try-catch to prevent
IllegalStateException from blocking release of remaining resources.
Lift extractor lifecycle to an outer try-finally in performConversion
so it is always released regardless of where an error occurs, and
remove redundant extractor release from performConversionBuffered.
Include a benchmark integration test that measures conversion
times for large audio files. The test assets are gitignored
(~146 MB) and must be provided locally — see the doc comment
in benchmark_test.dart for setup instructions.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Set ByteBuffer position/limit from bufferInfo before reading decoded
PCM in both performConversion and performConversionBuffered. This
prevents potential audio corruption on codecs that use non-zero offsets.

Also guard benchmark tests so they skip when large test assets are
not bundled, avoiding failures on clean checkouts.
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.

Implement streaming output to disk during decoding

2 participants