Standard library · Encoding and data

audio/mp3

Reading the frames, headers and VBR tags of an mp3 file.

import audio.mp3

An mp3 file is a run of frames, each with a header saying how it is coded and how long it is. This package reads those headers, so you can tell what a file is, how long it lasts, how many frames it holds and where in it a given second falls, all without decoding any sound.

That covers everything a library listing, a player's seek bar or a tag editor needs. Every MPEG version and layer is parsed, including the MPEG-2 and 2.5 half and quarter sample rates, and the Xing, Info and VBRI headers that say how long a variable bit rate file really is.

Things to know

  • Audio decoding is not here yet. This package reads the structure of an mp3, not its sound, and CanDecode in audio/codec says so. Layer III decoding needs the ISO Huffman and scalefactor tables, which are a separate and much larger job.
  • A frame is only trusted when another frame follows where it says one should, which is what stops FindSync locking onto a run of 0xFF bytes inside a tag.
  • The frame count in a Xing header does not include the frame the header itself sits in, so a file with 10 audio frames reports 11 when counted and 10 when asked.
  • For a constant bit rate file the length is frames times samples per frame. For a variable one, only the Xing header knows, which is why ProbeMP3 prefers it.
  • Tags are audio/tag's job, and this package uses it to find where the audio starts.

Types

struct Header
What one MPEG audio frame header says.
NameTypeDescription
versionint = 0Which MPEG version, one of the Version numbers.
layerint = 0Which layer, one of the Layer numbers.
bitrateint = 0The bit rate in kilobits per second.
rateint = 0Frames per second.
channelsint = 0How many channels, worked out from the mode.
modeint = 0The channel mode, one of the Mode numbers.
mode_extint = 0The mode extension, which says what joint stereo is doing.
paddingint = 01 when this frame carries one extra byte to keep the average bit rate.
crcbool = falseTrue when a CRC follows the header.
frame_sizeint = 0How many bytes the whole frame takes, padding included.
samplesint = 0How many PCM frames this frame decodes to.
mp3-Read.salam
import audio
import audio.mp3
import audio.tag
import compress

func push_frame(b &: compress.ByteBuf, h1: int, h2: int, h3: int, size: int):
    compress.BufPush(b, 255)
    compress.BufPush(b, h1)
    compress.BufPush(b, h2)
    compress.BufPush(b, h3)
    mut i := 4
    until i < size:
        compress.BufPush(b, 0)
        i += 1
    end
end

func main:
    mut cbr := compress.NewByteBuf()
    mut n := 0
    until n < 3:
        push_frame(cbr, 0xFB, 0x90, 0xC0, 417)
        n += 1
    end
    println compress.BufLen(cbr)
    println mp3.IsMP3(cbr.data, cbr.len), audio.FormatName(audio.DetectFormat(cbr.data, cbr.len))
    println tag.HasID3v2(cbr.data, cbr.len), mp3.AudioStart(cbr.data, cbr.len)

    mut h := mp3.Header {}
    println mp3.ParseHeader(cbr.data, cbr.len, 0, h)
    println h.version == mp3.Version1, h.layer == mp3.Layer3
    println h.rate, h.bitrate, h.channels, h.mode == mp3.ModeMono
    println h.samples, h.frame_size, h.padding, h.crc
    println mp3.CountFrames(cbr.data, cbr.len)

    mut pok := false
    info := mp3.ProbeMP3(cbr.data, cbr.len, pok)
    println pok, info.rate, info.channels, info.bits, info.lossless
    println info.frames

    println mp3.FrameAt(cbr.data, cbr.len, 0), mp3.FrameAt(cbr.data, cbr.len, 1)
    println mp3.FrameAt(cbr.data, cbr.len, 99)
    println mp3.SeekToSecond(cbr.data, cbr.len, 0.03) >= 0
    mut heads := mp3.Headers(cbr.data, cbr.len)
    println heads.len(), heads.get(0).rate, heads.get(2).bitrate
    heads.free()

    mut v2 := compress.NewByteBuf()
    mut p := 0
    until p < 2:
        push_frame(v2, 0xF3, 0x80, 0x00, 208)
        p += 1
    end
    mut vh := mp3.Header {}
    println mp3.ParseHeader(v2.data, v2.len, 0, vh)
    println vh.version == mp3.Version2, vh.rate, vh.channels, vh.samples, vh.frame_size
    println mp3.CountFrames(v2.data, v2.len)

    println mp3.SamplesPerFrame(mp3.Version1, mp3.Layer3), mp3.SamplesPerFrame(mp3.Version2, mp3.Layer3)
    println mp3.SamplesPerFrame(mp3.Version25, mp3.Layer3), mp3.SamplesPerFrame(mp3.Version1, mp3.Layer1)
    println mp3.SamplesPerFrame(mp3.Version1, mp3.Layer2)
    println mp3.ModeStereo, mp3.ModeJoint, mp3.ModeDual, mp3.ModeMono

    mut junk := compress.NewByteBuf()
    mut j := 0
    until j < 32:
        compress.BufPush(junk, j)
        j += 1
    end
    println mp3.FindSync(junk.data, junk.len, 0), mp3.IsMP3(junk.data, junk.len)
    println mp3.CountFrames(junk.data, junk.len)
    mut bad := mp3.Header {}
    println mp3.ParseHeader(junk.data, junk.len, 0, bad)
    mut nok := false
    mp3.ProbeMP3(junk.data, junk.len, nok)
    println nok
    mut vn := mp3.VbrInfo {}
    println mp3.ReadVbrInfo(cbr.data, cbr.len, vn), vn.found

    compress.BufFree(cbr)
    compress.BufFree(v2)
    compress.BufFree(junk)
end
Output1251 true mp3 false 0 true true true 44100 128 1 true 1152 417 0 false 3 true 44100 1 16 false 3456 0 417 -1 true 3 44100 128 true true 22050 2 576 208 2 1152 576 576 384 1152 0 1 2 3 -1 false 0 false false false false

VbrInfo

struct VbrInfo
What a Xing, Info or VBRI header says about the file as a whole.
NameTypeDescription
framesint = 0How many audio frames follow. The header's own frame is not counted.
bytesint = 0How many bytes of audio there are.
vbrbool = falseTrue for a Xing header, which means the bit rate varies.
qualityint = 0The quality number the encoder recorded.
delayint = 0How many samples the encoder added at the front, which gapless playback must drop.
paddingint = 0How many samples it added at the end.
foundbool = falseTrue when such a header was there at all.

The example for this item is shown with the ones above: mp3-Read.salam

Constants and variables

MPEG version numbers Version…

Which MPEG version a frame header declares.
NameValue
Version250
Version22
Version13

The example for this item is shown with the ones above: mp3-Read.salam

Layer numbers Layer…

Which layer a frame header declares. An mp3 file is Layer III.
NameValue
Layer13
Layer22
Layer31

The example for this item is shown with the ones above: mp3-Read.salam

Channel mode numbers Mode…

How a frame uses its two channels.
NameValue
ModeStereo0
ModeJoint1
ModeDual2
ModeMono3

The example for this item is shown with the ones above: mp3-Read.salam

Functions

SamplesPerFrame

func SamplesPerFrame(version: int, layer: int): int
How many PCM frames one MPEG frame holds, which depends on the version and the layer.

The example for this item is shown with the ones above: mp3-Read.salam

ParseHeader

func ParseHeader(p: void*, n: int, at: int, out &: Header): bool
Reads one frame header and works out its size, or says this is not a frame.

The example for this item is shown with the ones above: mp3-Read.salam

FindSync

func FindSync(p: void*, n: int, from: int): int
The next frame from a position, confirmed by checking that a second frame follows where the first says it should.

The example for this item is shown with the ones above: mp3-Read.salam

AudioStart

func AudioStart(p: void*, n: int): int
Where the first frame is, past any ID3v2 tag.

The example for this item is shown with the ones above: mp3-Read.salam

IsMP3

func IsMP3(p: void*, n: int): bool
True when this looks like an mp3: an ID3v2 tag, or a frame that checks out.

The example for this item is shown with the ones above: mp3-Read.salam

CountFrames

func CountFrames(p: void*, n: int): int
How many frames the file holds, by walking all of them.

The example for this item is shown with the ones above: mp3-Read.salam

ReadVbrInfo

func ReadVbrInfo(p: void*, n: int, out &: VbrInfo): bool
Reads a Xing, Info or VBRI header, which is the only cheap way to know how long a variable bit rate file is.

The example for this item is shown with the ones above: mp3-Read.salam

ProbeMP3

func ProbeMP3(p: void*, n: int, ok &: bool): audio.Info
Everything a listing needs: the rate, the channels, the length and the bit rate, from the headers alone.

The example for this item is shown with the ones above: mp3-Read.salam

FrameAt

func FrameAt(p: void*, n: int, index: int): int
Where a frame is by index, or -1 when there is no such frame.

The example for this item is shown with the ones above: mp3-Read.salam

SeekToSecond

func SeekToSecond(p: void*, n: int, seconds: f64): int
Where the frame covering a time is, which is how you start playing from the middle.

The example for this item is shown with the ones above: mp3-Read.salam

Headers

func Headers(p: void*, n: int): Vector<Header>
Every frame header, in order. Free the vector when you are done.

The example for this item is shown with the ones above: mp3-Read.salam

ProbeFileMP3

func ProbeFileMP3(path: str, ok &: bool): audio.Info
The same from a path.

The example for this item is shown with the ones above: mp3-Read.salam