Last active
April 18, 2025 14:41
-
-
Save LucasAlfare/5540075fdea7f4e94c4d4ba0bb59c253 to your computer and use it in GitHub Desktop.
Custom MP3 decoder from Scratch in Kotlin // Rascunho de decodificador de MP3 PRÓPRIO do ZERO
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@file:OptIn(ExperimentalUnsignedTypes::class) | |
import com.lucasalfare.flbinary.Reader | |
import com.lucasalfare.flbinary.readBits | |
import java.io.File | |
val bitrateTable = IntArray(0b1111) { -1 } | |
val sampleRateTable = IntArray(0b11) { -1 } | |
fun main() { | |
bitrateTable[0b0001] = 32000 | |
bitrateTable[0b0010] = 40000 | |
bitrateTable[0b0011] = 48000 | |
bitrateTable[0b0100] = 56000 | |
bitrateTable[0b0101] = 64000 | |
bitrateTable[0b0110] = 80000 | |
bitrateTable[0b0111] = 96000 | |
bitrateTable[0b1000] = 112000 | |
bitrateTable[0b1001] = 128000 | |
bitrateTable[0b1010] = 160000 | |
bitrateTable[0b1011] = 192000 | |
bitrateTable[0b1100] = 224000 | |
bitrateTable[0b1101] = 256000 | |
bitrateTable[0b1110] = 320000 | |
sampleRateTable[0b00] = 44100 | |
sampleRateTable[0b01] = 48000 | |
sampleRateTable[0b10] = 32000 | |
val bytes = File("bip.mp3").readBytes().toUByteArray() | |
val reader = Reader(bytes) | |
skipTags(reader) | |
println("Reading audio data...") | |
var n = 0 | |
while (reader.position < bytes.size - 1) { | |
val frameSynchronizer = reader.readBits(11) | |
if (frameSynchronizer == 0b11111111_111L) { | |
println("Found a frame header at position ${reader.position - 1}!") | |
readAudioFrame(reader) | |
println() | |
n++ | |
if (n == 10) break | |
} else { | |
reader.position-- | |
} | |
} | |
} | |
fun skipTags(reader: Reader) { | |
val id3Signature = reader.readString(3) | |
if (id3Signature == "ID3") { | |
val versionMajor = reader.read1Byte() | |
val versionMinor = reader.read1Byte() | |
val flags = reader.read1Byte() | |
val b3 = reader.read1Byte() | |
val b2 = reader.read1Byte() | |
val b1 = reader.read1Byte() | |
val b0 = reader.read1Byte() | |
val tagSize = ((b3 and 0x7F) shl 21) or | |
((b2 and 0x7F) shl 14) or | |
((b1 and 0x7F) shl 7) or | |
(b0 and 0x7F) | |
val hasExtendedHeader = (flags and 0x40) != 0 | |
var totalSize = tagSize + 10 | |
if (hasExtendedHeader) { | |
val extHeaderSize = (reader.read1Byte() shl 24) or | |
(reader.read1Byte() shl 16) or | |
(reader.read1Byte() shl 8) or | |
reader.read1Byte() | |
val skipSize = if (versionMajor == 3) extHeaderSize - 4 else extHeaderSize | |
reader.advancePosition(skipSize) | |
totalSize += extHeaderSize | |
} | |
reader.advancePosition(totalSize - 10) | |
println("Tag skipped ($totalSize bytes).") | |
} else { | |
reader.position -= 3 | |
println("No tags found.") | |
} | |
} | |
fun readAudioFrame(reader: Reader) { | |
reader.position -= 2 | |
// next bytes after frame synchronizer | |
// we care only about the bytes 1/2/3 because byte 0 is filled with bits '1'. | |
val headerBytes: UByteArray = reader.readBytes(4) | |
val b2 = headerBytes[1].toInt() | |
val b3 = headerBytes[2].toInt() | |
val b4 = headerBytes[3].toInt() | |
val mpegVersionID = (b2 shr 5) and 0b11 | |
val layer = (b2 shr 3) and 0b11 | |
val hasCrcProtection = (b2 shr 2) and 0b1 | |
val bitrateIndex = (b3 shr 4) and 0b1111 | |
val samplingRateFrequencyIndex = (b3 shr 2) and 0b11 | |
val hasPadding = (b3 shr 1) and 0b1 | |
val privateBit = b3 and 0b1 | |
val channelMode = (b4 shr 6) and 0b11 | |
val modeExtension = (b4 shr 4) and 0b11 | |
val isCopyrighted = (b4 shr 3) and 0b1 | |
val isOriginal = (b4 shr 2) and 0b1 | |
val emphasis = b4 and 0b11 | |
val bitrate = bitrateTable[bitrateIndex] | |
val sampleRate = sampleRateTable[samplingRateFrequencyIndex] | |
val currentFrameLength = (144 * bitrate / sampleRate) + hasPadding | |
// println("[BB] modeExtension: ${mpegVersionID.toString(2).padStart(2, '0')}") | |
// println("[CC] layer: ${layer.toString(2).padStart(2, '0')}") | |
// println("[D] hasCrcProtection: ${hasCrcProtection.toString(2).padStart(1, '0')}") | |
// println("[EEEE] bitrateIndex:${bitrateIndex.toString(2).padStart(4, '0')}") | |
// println("[FF] samplingRateFrequencyIndex: ${samplingRateFrequencyIndex.toString(2).padStart(2, '0')}") | |
// println("[G] hasPadding: ${hasPadding.toString(2).padStart(1, '0')}") | |
// println("[H] privateBit: ${privateBit.toString(2).padStart(1, '0')}") | |
// println("[II] channelMode: ${channelMode.toString(2).padStart(2, '0')}") | |
// println("[JJ] modeExtension: ${modeExtension.toString(2).padStart(2, '0')}") | |
// println("[K] isCopyrighted: ${isCopyrighted.toString(2).padStart(1, '0')}") | |
// println("[L] isOriginal: ${isOriginal.toString(2).padStart(1, '0')}") | |
// println("[MM] emphasis: ${emphasis.toString(2).padStart(2, '0')}") | |
println("Decoded bitrate: $bitrate") | |
println("Decoded sampleRate: $sampleRate") | |
println("Total frame length (in bytes): $currentFrameLength") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment