# How to Encode Audio Samples to MP3 Using Mp3Encoder and WavParser in Muse

> Learn how Muse encodes audio samples to MP3 using Mp3Encoder and WavParser. Process PCM data from WAV files into MP3 streams efficiently with this guide.

- Repository: [Ko Shin/muse](https://github.com/kkoshin/muse)
- Tags: how-to-guide
- Published: 2026-03-05

---

**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](https://github.com/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)](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 a `ShortArray`
- **`readStereo(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)](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 a `ShortArray`, then passes it to `lame.encode(buffer, buffer, samplesRead, mp3Buffer)`
- **Stereo**: Calls `wavParser.readStereo(leftBuffer, rightBuffer, CHUNK_SIZE)` to fill separate arrays, then passes both to `lame.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)](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:

```kotlin
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:

```kotlin
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

- **`WavParser`** parses WAV headers in [[`WavParser.kt`](https://github.com/kkoshin/muse/blob/main/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 `readMono` and `readStereo`, which internally call `readSamples` to decode 8-bit or 16-bit PCM into `ShortArray` buffers.
- **`Mp3Encoder`** provides platform-specific implementations in [[`Mp3Encoder.android.kt`](https://github.com/kkoshin/muse/blob/main/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/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 using `lame_encode_buffer()` from the native C library.
- The `AudioExportPipeline` coordinates the entire flow, connecting the `BufferedSource` from the WAV file to the `BufferedSink` of 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.