Case Study: MP3

Case Study: MP3#

The MP3 format is widely used to encode audio.

The PNG spec mp3.fan can be directly used in Fandango:

$ fandango fuzz -f mp3.fan -n 1 --population-size=1 -o silence.mp3

produces a second of silence in silence.mp3.

The file mp3.fan is reproduced verbatim below.

Added in version 1.1: png.fan requires Fandango 1.1 or later.

# ============================================================
# Minimal Valid MP3 File (ID3v2 + MPEG1 Layer III + ID3v1)
# Produces exactly one second of silence.
# ============================================================

import struct
import random


# ============================================================
# Global Constants
# ============================================================

# MPEG-1 Layer III, 128 kbps, 44.1 kHz, no CRC, stereo
BITRATE_INDEX = 9              # 128 kbps
SAMPLING_INDEX = 0             # 44.1 kHz
PADDING_BIT = 0
CHANNEL_MODE = 0               # stereo
SAMPLES_PER_FRAME = 1152
SAMPLING_RATE = 44100
BITRATE = 128000
FRAME_LENGTH = 417             # floor(144*bitrate/sr)
FRAMES_PER_SECOND = 38         # ceil(44100/1152)
TEXT_DIMENSION = 100           # required preference


# ============================================================
# Helper Functions
# ============================================================

def synchsafe(n: int) -> bytes:
    """Convert integer to 4-byte synchsafe representation."""
    return bytes([
        (n >> 21) & 0x7F,
        (n >> 14) & 0x7F,
        (n >> 7) & 0x7F,
        n & 0x7F,
    ])


# ============================================================
# Grammar
# ============================================================

# ------------------------------------------------------------
# Start Symbol
# ------------------------------------------------------------

<start> ::= <mp3_file>


# ------------------------------------------------------------
# Complete MP3 File
# ------------------------------------------------------------

<mp3_file> ::= <id3v2> <mpeg_audio> <id3v1>

# ------------------------------------------------------------
# MPEG Audio
# ------------------------------------------------------------

<mpeg_audio> ::= <mpeg_audio_frame>{FRAMES_PER_SECOND}


# ------------------------------------------------------------
# MPEG Audio Frame
# ------------------------------------------------------------

# <mpeg_audio_frame> ::= <byte>* := build_frame()

<mpeg_audio_frame> ::= \
    <mpeg_audio_header> \
    <mpeg_audio_side_info> \
    <mpeg_audio_main_data>

# Frame Header (4 bytes, fully bit modeled)
<mpeg_audio_header> ::= \
    <mpeg_audio_sync_bits> \
    <mpeg_audio_version_id> \
    <mpeg_audio_layer_bits> \
    <mpeg_audio_protection_bit> \
    <mpeg_audio_bitrate_bits> \
    <mpeg_audio_sampling_bits> \
    <mpeg_audio_padding_bit> \
    <mpeg_audio_private_bit> \
    <mpeg_audio_channel_mode_bits> \
    <mpeg_audio_mode_extension_bits> \
    <mpeg_audio_copyright_bit> \
    <mpeg_audio_original_bit> \
    <mpeg_audio_emphasis_bits>


# 11 sync bits: 11111111111
<mpeg_audio_sync_bits> ::= 1 1 1 1 1 1 1 1 1 1 1

# MPEG Version 1: 11
<mpeg_audio_version_id> ::= 1 1

# Layer III: 01
<mpeg_audio_layer_bits> ::= 0 1

# No CRC
<mpeg_audio_protection_bit> ::= 1

# 128 kbps: 1001
<mpeg_audio_bitrate_bits> ::= 1 0 0 1

# 44.1 kHz: 00
<mpeg_audio_sampling_bits> ::= 0 0

# No padding
<mpeg_audio_padding_bit> ::= 0

<mpeg_audio_private_bit> ::= 0

# Stereo: 00
<mpeg_audio_channel_mode_bits> ::= 0 0

# Mode extension (unused in stereo)
<mpeg_audio_mode_extension_bits> ::= 0 0

<mpeg_audio_copyright_bit> ::= 0
<mpeg_audio_original_bit> ::= 0

# No emphasis: 00
<mpeg_audio_emphasis_bits> ::= 0 0

  
# Side information (32 bytes for stereo MPEG1)
<mpeg_audio_side_info> ::= b"\x00"{32}

# Main data (rest of frame) - all silence
MAIN_DATA_SIZE = FRAME_LENGTH - 4 - 32
<mpeg_audio_main_data> ::= b"\x00"{MAIN_DATA_SIZE}

# <mpeg_audio_main_data> ::= <byte>{MAIN_DATA_SIZE} := white_noise(MAIN_DATA_SIZE)
#
# def white_noise(size: int) -> bytes:
#     return bytes([random.randint(1, 255) for _ in range(size)])


# ------------------------------------------------------------
# ID3v2 Section
# ------------------------------------------------------------

# This is a minimal ID3v2.3 tag with one TIT2 frame

<id3v2> ::= <id3v2_header> <id3v2_frame>

# <id3v2> ::= <byte>* := build_id3v2()

<id3v2_header> ::= b"ID3" b"\x03\x00" b"\x00" <id3v2_len>

<id3v2_len> ::= <byte>{4} 
where <id3v2_len> == synchsafe(len(bytes(<id3v2_frame>)))

<id3v2_frame> ::= \
    <id3v2_frame_id> \
    <id3v2_frame_size> \
    <id3v2_flags> \
    <id3v2_encoding> \
    <id3v2_text>
    
<id3v2_frame_id> ::= b"TIT2"

<id3v2_frame_size> ::= <byte>{4}
where <id3v2_frame_size> == \
    struct.pack(">I", len(bytes(<id3v2_encoding>)) + 
                      len(bytes(<id3v2_text>)))

<id3v2_flags> ::= b"\x00\x00"

<id3v2_encoding> ::= b"\x00"

<id3v2_text> ::= <byte>* := \
    b"Silent MP3".ljust(TEXT_DIMENSION, b"\x00")


# ------------------------------------------------------------
# ID3v1 Section
# ------------------------------------------------------------

# <id3v1> ::= <byte>* := build_id3v1()

<id3v1> ::= \
    <id3v1_tag> \
    <id3v1_title> \
    <id3v1_artist> \
    <id3v1_album> \
    <id3v1_year> \
    <id3v1_comment> \
    <id3v1_genre>

<id3v1_tag> ::= b"TAG"

<id3v1_title> ::= <byte>{30} := b"Silent MP3".ljust(30, b"\x00")

<id3v1_artist> ::= <byte>{30} := b"ZZ Top".ljust(30, b"\x00")

<id3v1_album> ::= <byte>{30} := b"Fandango!".ljust(30, b"\x00")

<id3v1_year> ::= <byte>{4} := b"2026"

<id3v1_comment> ::= <byte>{30} := b"".ljust(30, b"\x00")

<id3v1_genre> ::= b"\x00"

Note

Note that in mp3.fan, almost all fields have fixed values, so it creates only silent MP3 fields. Feel free to extend it to increase diversity.