How to Encode Audio Samples to MP3 Using Mp3Encoder and WavParser in Muse
The Muse app converts raw PCM data from WAV files into MP3 streams by using WavParser to extract sample metadata and PCM buffers from an okio.BufferedSource, then feeding those buffers to platform-specific Mp3Encoder implementations that wrap the LAME library to produce MP3 bytes written to an okio.BufferedSink.
In the Kotlin Multiplatform project kkoshin/muse, the audio export pipeline relies on two core components to transform uncompressed audio into compressed MP3 format. This guide explains how WavParser handles PCM sample extraction and how Mp3Encoder processes those samples on both Android and iOS platforms.
Understanding the WAV Parsing Pipeline
The WavParser class, located in [muse/src/commonMain/kotlin/io/github/kkoshin/muse/audio/WavParser.kt](https://github.com/kkoshin/muse/blob/main/muse/src/commonMain/kotlin/io/github/kkoshin/muse/audio/WavParser.kt), is responsible for parsing the WAV file structure and exposing raw audio samples for encoding.
Header Validation and Metadata Extraction
The parser begins by reading the RIFF header and verifying the file is a valid PCM WAV. It parses the fmt chunk to extract critical metadata required for encoding:
- Sample rate: The frequency in Hz (e.g., 44100)
- Channels: Mono (1) or Stereo (2)
- Sample bits: Bit depth (8 or 16)
After parsing the header, WavParser locates the data chunk and records its size, positioning the read pointer at the start of the raw PCM stream.
Reading PCM Samples
WavParser provides two primary methods for consuming audio data:
readMono(dst: ShortArray, numSamples: Int): Reads mono PCM samples into aShortArrayreadStereo(left: ShortArray, right: ShortArray, numSamples: Int): De-interleaves stereo PCM data into separate left and right channel arrays
Both methods delegate to an internal readSamples function that processes the BufferedSource in chunks of up to 8192 bytes. The parser automatically handles 8-bit and 16-bit PCM formats through decodeSamples8bit and decodeSamples16bit, converting them to signed 16-bit integers required by the LAME encoder.
Encoding Audio to MP3 with Mp3Encoder
The Mp3Encoder interface has platform-specific implementations that wrap the LAME MP3 encoder. Both implementations follow the same pattern: initialize the encoder with metadata from WavParser, then stream PCM data through LAME's encoding functions.
Android Implementation
The Android encoder in [muse/src/androidMain/kotlin/io/github/kkoshin/muse/audio/Mp3Encoder.android.kt](https://github.com/kkoshin/muse/blob/main/muse/src/androidMain/kotlin/io/github/kkoshin/muse/audio/Mp3Encoder.android.kt) uses the com.naman14.androidlame library.
The implementation initializes AndroidLame with ID3 tags (artist defaults to "μ's" with the current year), then selects the appropriate encoding path based on channel count:
- Mono: Calls
wavParser.readMono(buffer, CHUNK_SIZE)to fill aShortArray, then passes it tolame.encode(buffer, buffer, samplesRead, mp3Buffer) - Stereo: Calls
wavParser.readStereo(leftBuffer, rightBuffer, CHUNK_SIZE)to fill separate arrays, then passes both tolame.encode(leftBuffer, rightBuffer, samplesRead, mp3Buffer)
After processing all samples, lame.flush(mp3Buffer) writes any remaining MP3 frames to the output sink.
iOS Implementation
The iOS encoder in [muse/src/iosMain/kotlin/io/github/kkoshin/muse/audio/Mp3Encoder.ios.kt](https://github.com/kkoshin/muse/blob/main/muse/src/iosMain/kotlin/io/github/kkoshin/muse/audio/Mp3Encoder.ios.kt) uses the cocoapods.lame C library directly through Kotlin/Native interop.
The encoder initializes LAME using lame_init(), configures the sample rate and channels with lame_set_in_samplerate() and lame_set_num_channels(), then finalizes parameters with lame_init_params(). The encoding loop mirrors Android's logic but calls lame_encode_buffer() for mono streams or lame_encode_buffer() with interleaved false for stereo streams, passing the ShortArray pointers from WavParser.
End-to-End Audio Export Implementation
The AudioExportPipeline orchestrates the conversion by instantiating WavParser from an input source, selecting the appropriate Mp3Encoder implementation, and managing the BufferedSink for the output file.
Here is a complete example showing how to encode a WAV file to MP3 using these components:
import okio.buffer
import okio.source
import okio.sink
import io.github.kkoshin.muse.audio.WavParser
import io.github.kkoshin.muse.audio.Mp3Encoder
import io.github.kkoshin.muse.audio.Mp3Metadata
import kotlinx.coroutines.runBlocking
import java.io.File
fun encodeWavToMp3(inputWav: File, outputMp3: File) {
// Initialize the WAV parser with the source file
val bufferedSource = inputWav.source().buffer()
val parser = WavParser(bufferedSource)
// Prepare the output sink
val bufferedSink = outputMp3.sink().buffer()
// Create encoder with ID3 metadata
val encoder = Mp3Encoder()
val metadata = Mp3Metadata(
id3TagArtist = "MuseApp",
id3TagYear = java.time.Year.now().value.toString()
)
runBlocking {
// Stream PCM data from parser to MP3 sink
encoder.encode(parser, bufferedSink, metadata)
}
// Ensure all resources are closed
bufferedSink.close()
parser.close()
}
For streaming scenarios where you process audio bytes in memory without intermediate files:
fun encodeWavBytesToMp3(wavBytes: ByteArray): ByteArray {
val source = wavBytes.inputStream().source().buffer()
val parser = WavParser(source)
// Use a Pipe for streaming output
val pipe = okio.Pipe()
val encoder = Mp3Encoder()
runBlocking {
// Encode in background
launch {
encoder.encode(parser, pipe.sink.buffer(), Mp3Metadata("Stream", "2024"))
}
// Read MP3 bytes as they become available
pipe.source.readByteArray()
}
}
Summary
WavParserparses WAV headers in [WavParser.kt](https://github.com/kkoshin/muse/blob/main/muse/src/commonMain/kotlin/io/github/kkoshin/muse/audio/WavParser.kt), validating PCM format and extracting metadata (sampleRate, channels, sampleBits) needed for MP3 encoding.- The parser streams samples through
readMonoandreadStereo, which internally callreadSamplesto decode 8-bit or 16-bit PCM intoShortArraybuffers. Mp3Encoderprovides platform-specific implementations in [Mp3Encoder.android.kt](https://github.com/kkoshin/muse/blob/main/muse/src/androidMain/kotlin/io/github/kkoshin/muse/audio/Mp3Encoder.android.kt) and [Mp3Encoder.ios.kt](https://github.com/kkoshin/muse/blob/main/muse/src/iosMain/kotlin/io/github/kkoshin/muse/audio/Mp3Encoder.ios.kt), wrapping the LAME library to convert PCM buffers to MP3 format.- Both implementations handle mono and stereo channels differently, with Android using
AndroidLame.encode()and iOS usinglame_encode_buffer()from the native C library. - The
AudioExportPipelinecoordinates the entire flow, connecting theBufferedSourcefrom the WAV file to theBufferedSinkof the MP3 output through these encoder classes.
Frequently Asked Questions
What audio formats does WavParser support?
WavParser specifically supports uncompressed PCM WAV files (audio format code 1) with 8-bit or 16-bit sample depths. It validates the RIFF/WAVE header structure and parses the fmt chunk to ensure the data is linear PCM before allowing sample extraction through readMono or readStereo.
How does Mp3Encoder handle different channel configurations?
The encoder detects the channel count from the WavParser metadata and selects the appropriate LAME function. For mono files, it uses writeMonoAudio (Android) or lame_encode_buffer with a single channel pointer (iOS). For stereo, it uses writeStereoAudio (Android) or lame_encode_buffer with separate left/right arrays (iOS), ensuring proper interleaving or de-interleaving as required by the LAME specification.
Can I use Mp3Encoder with sources other than WavParser?
While Mp3Encoder.encode() accepts any WavParser instance, the encoder expects PCM data in 16-bit signed integer format at a consistent sample rate. If you have raw PCM data from another source, you could construct a custom parser that implements the same sample reading interface, though the current implementation is tightly coupled to the WAV parsing logic for metadata extraction.
Where is the LAME library configured in the Muse project?
On Android, the LAME library is included via the com.naman14.androidlame dependency configured in the Android-specific source set. On iOS, it is integrated through the cocoapods.lame CocoaPod, allowing the Kotlin/Native code to import C functions directly via import cocoapods.lame.* in the iOS implementation file.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →