Task 414: .MP3 File Format

Task 414: .MP3 File Format

MP3 File Format Specifications

The MP3 file format (MPEG-1/2 Audio Layer III) is a lossy audio compression standard defined in ISO/IEC 11172-3 (MPEG-1) and ISO/IEC 13818-3 (MPEG-2), with an unofficial MPEG-2.5 extension for lower sampling rates. MP3 files consist of a sequence of independent frames (each typically 26 ms long), optional ID3 metadata tags (v1 at the end or v2 at the beginning), and for variable bitrate (VBR) files, a special info frame like the Xing header. There is no overall file header; playback starts by syncing to the first frame. Frames include a 4-byte header followed by audio data (and optional 16-bit CRC). ID3 tags store metadata separately from the audio stream.

Key technical details:

  • Frames: Each frame has a fixed number of samples (1152 for MPEG-1 Layer III, 576 for MPEG-2/2.5). Frame length varies by bitrate and sampling rate, calculated as int((144 * bitrate * 1000 / sampling_rate) + padding).
  • Compression: Uses perceptual coding, MDCT, and psychoacoustics to discard inaudible data, achieving 75-95% size reduction compared to uncompressed audio.
  • Bitrates: 8-320 kbps (depending on version/layer).
  • Sampling Rates: 8-48 kHz (MPEG-1: 32/44.1/48 kHz; MPEG-2: 16/22.05/24 kHz; MPEG-2.5: 8/11.025/12 kHz).
  • Channels: Mono, stereo, joint stereo, dual channel.
  • VBR Support: Allowed, with optional Xing/Info header for seeking and duration info.
  • ID3v1: Fixed 128-byte tag at file end.
  • ID3v2: Variable-length tag at file start, with multiple frames for metadata.
  1. List of Properties Intrinsic to the File Format

These are the core structural and metadata properties extracted from the MP3 file structure, including frame headers, VBR info, and ID3 tags. They define the audio configuration and metadata without relying on external file system attributes (e.g., no OS timestamps or permissions).

Audio Frame Header Properties (per frame; typically consistent across file):

  • MPEG Version (e.g., 1, 2, 2.5)
  • Layer (e.g., III)
  • Protection (CRC protected: yes/no)
  • Bitrate (kbps, e.g., 128, 192, 320; or "free" for custom)
  • Sampling Rate (Hz, e.g., 44100, 48000)
  • Padding (yes/no; adds 1 byte/slot)
  • Private Bit (application-specific: 0/1)
  • Channel Mode (Stereo, Joint Stereo, Dual Channel, Mono)
  • Mode Extension (for Joint Stereo: combinations of Intensity Stereo and MS Stereo on/off)
  • Copyright (yes/no)
  • Original (yes/no)
  • Emphasis (None, 50/15 ms, CCIT J.17, Reserved)

VBR/Xing Header Properties (if present in first frame for VBR files):

  • VBR Identifier ("Xing" or "Info")
  • Flags (Frames, Bytes, TOC, VBR Scale)
  • Number of Frames
  • File Length (bytes)
  • TOC (Table of Contents for seeking; 100 entries)
  • VBR Scale (0-100; encoder-specific quality indicator)

ID3v1 Tag Properties (if present; fixed strings):

  • Title (30 chars)
  • Artist (30 chars)
  • Album (30 chars)
  • Year (4 chars)
  • Comment (28 or 30 chars; v1.1 uses 28 for track number support)
  • Track Number (if v1.1; byte value)
  • Genre (byte index; e.g., 0=Blues, 12=Dance, 80=80's)

ID3v2 Tag Properties (if present; common frames):

  • Title (TIT2)
  • Artist (TPE1)
  • Album (TALB)
  • Year/Release Date (TYER or TDRC)
  • Track Number (TRCK, e.g., "3/12")
  • Genre (TCON, e.g., "(12)Dance" or free text)
  • Composer (TCOM)
  • Original Artist (TOPE)
  • Copyright (TCOP)
  • Encoded By (TENC)
  • Comments (COMM; includes language and description)
  • URL (WXXX or others like WOAR for artist URL)
  • And potentially others (e.g., APIC for attached picture, but focused on text properties here)

Additional derived properties (calculated from headers/tags):

  • Duration (seconds; from number of frames * samples per frame / sampling rate)
  • Variable/Constant Bitrate (VBR/CBR)
  • File Size (bytes)
  • ID3 Version (v1, v2, or both)
  1. Two Direct Download Links for .MP3 Files

These are public domain audio files available for free download.

  1. Ghost Blog Embedded HTML/JavaScript for Drag-and-Drop MP3 Property Dump

This is a self-contained HTML snippet with JavaScript that can be embedded in a Ghost blog post. It creates a drop zone where users can drag an MP3 file. The script reads the file binary, parses key properties (focusing on frame header from first frame, ID3v1 if present, ID3v2 basics, and VBR if detected), and dumps them to the screen in a pre-formatted block. Note: This is browser-based (uses FileReader); writing/modifying files is not supported in browser JS for security reasons—use Node.js for that.

Drag and drop an MP3 file here

  1. Python Class for MP3 Property Handling

This Python class uses built-in modules (no external libs needed) to open an MP3 file, decode/read the properties, print them to console, and write (modify select properties like ID3v1 title and save a new file).

import struct
import os

class MP3Parser:
    def __init__(self, filepath):
        self.filepath = filepath
        with open(filepath, 'rb') as f:
            self.buffer = f.read()
        self.props = self._parse()

    def _parse(self):
        props = {}
        buffer = self.buffer
        # ID3v2
        if buffer[:3] == b'ID3':
            major = buffer[3]
            size = (buffer[6] << 21) | (buffer[7] << 14) | (buffer[8] << 7) | buffer[9]
            offset = 10
            props['id3v2'] = {}
            while offset < size + 10:
                frame_id = buffer[offset:offset+4].decode('ascii', errors='ignore')
                if not frame_id.isalnum(): break
                frame_size = struct.unpack('>I', buffer[offset+4:offset+8])[0]
                frame_data = buffer[offset+10:offset+10+frame_size].decode('utf-8', errors='ignore').rstrip('\x00')
                props['id3v2'][frame_id] = frame_data
                offset += 10 + frame_size
        # ID3v1
        end = len(buffer)
        if buffer[end-128:end-125] == b'TAG':
            props['id3v1'] = {
                'title': buffer[end-125:end-95].decode('ascii', errors='ignore').rstrip('\x00 '),
                'artist': buffer[end-95:end-65].decode('ascii', errors='ignore').rstrip('\x00 '),
                'album': buffer[end-65:end-35].decode('ascii', errors='ignore').rstrip('\x00 '),
                'year': buffer[end-35:end-31].decode('ascii', errors='ignore').rstrip('\x00 '),
                'comment': buffer[end-31:end-3].decode('ascii', errors='ignore').rstrip('\x00 '),
                'track': buffer[end-2] if buffer[end-3] == 0 else None,
                'genre': buffer[end-1]
            }
        # First frame header
        header_offset = props.get('id3v2') and 10 + size or 0
        while header_offset < end - 4:
            if buffer[header_offset] == 0xFF and (buffer[header_offset + 1] & 0xE0) == 0xE0:
                break
            header_offset += 1
        if header_offset < end - 4:
            header = buffer[header_offset:header_offset+4]
            version_bits = (header[1] >> 3) & 3
            props['mpeg_version'] = 1 if version_bits == 3 else 2 if version_bits == 2 else 2.5
            props['layer'] = 4 - ((header[1] >> 1) & 3)
            props['protection'] = (header[1] & 1) == 0
            bitrate_index = header[2] >> 4
            bitrate_table = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0]  # MPEG1 L3
            props['bitrate'] = bitrate_table[bitrate_index]
            sample_index = (header[2] >> 2) & 3
            sample_table = [44100, 48000, 32000, 0]
            props['sampling_rate'] = sample_table[sample_index]
            props['padding'] = (header[2] >> 1) & 1
            props['private_bit'] = header[2] & 1
            channel_bits = header[3] >> 6
            props['channel_mode'] = ['Stereo', 'Joint stereo', 'Dual channel', 'Mono'][channel_bits]
            props['mode_extension'] = (header[3] >> 4) & 3
            props['copyright'] = (header[3] >> 3) & 1
            props['original'] = (header[3] >> 2) & 1
            props['emphasis'] = header[3] & 3
            # VBR Xing
            xing_offset = header_offset + (36 if props['mpeg_version'] == 1 and props['channel_mode'] != 'Mono' else 21)
            if buffer[xing_offset:xing_offset+4] in (b'Xing', b'Info'):
                props['vbr'] = True
                flags = struct.unpack('>I', buffer[xing_offset+4:xing_offset+8])[0]
                props['vbr_flags'] = flags
                props['num_frames'] = struct.unpack('>I', buffer[xing_offset+8:xing_offset+12])[0]
            else:
                props['vbr'] = False
        props['file_size'] = len(buffer)
        props['duration'] = props.get('num_frames') and (props['num_frames'] * 1152 / props['sampling_rate']) or 'Unknown (CBR)'
        return props

    def print_properties(self):
        import json
        print(json.dumps(self.props, indent=4))

    def write(self, new_filepath, modifications={}):
        buffer = bytearray(self.buffer)
        # Example: Modify ID3v1 title if present
        end = len(buffer)
        if 'id3v1' in self.props and modifications.get('title'):
            new_title = modifications['title'].ljust(30, ' ')[:30].encode('ascii')
            buffer[end-125:end-95] = new_title
        # Add more modifications as needed (e.g., ID3v2 is more complex)
        with open(new_filepath, 'wb') as f:
            f.write(buffer)
        print(f"Modified file saved to {new_filepath}")

# Example usage:
# parser = MP3Parser('example.mp3')
# parser.print_properties()
# parser.write('modified.mp3', {'title': 'New Title'})
  1. Java Class for MP3 Property Handling

This Java class uses java.io for binary reading, parses properties, prints to console, and supports writing (modifying e.g., ID3v1 title and saving).

import java.io.*;
import java.nio.*;
import java.nio.file.*;
import java.util.*;

public class MP3Parser {
    private byte[] buffer;
    private Map<String, Object> props;

    public MP3Parser(String filepath) throws IOException {
        buffer = Files.readAllBytes(Paths.get(filepath));
        props = parse();
    }

    private Map<String, Object> parse() {
        Map<String, Object> props = new HashMap<>();
        // ID3v2
        if (new String(buffer, 0, 3).equals("ID3")) {
            int major = buffer[3] & 0xFF;
            int size = ((buffer[6] & 0x7F) << 21) | ((buffer[7] & 0x7F) << 14) | ((buffer[8] & 0x7F) << 7) | (buffer[9] & 0x7F);
            int offset = 10;
            Map<String, String> id3v2 = new HashMap<>();
            while (offset < size + 10) {
                String frameId = new String(buffer, offset, 4);
                if (!frameId.matches("^[A-Z0-9]{4}$")) break;
                int frameSize = ByteBuffer.wrap(buffer, offset + 4, 4).getInt();
                String frameData = new String(buffer, offset + 10, frameSize).trim().replaceAll("\0", "");
                id3v2.put(frameId, frameData);
                offset += 10 + frameSize;
            }
            props.put("id3v2", id3v2);
        }
        // ID3v1
        int end = buffer.length;
        if (new String(buffer, end - 128, 3).equals("TAG")) {
            Map<String, Object> id3v1 = new HashMap<>();
            id3v1.put("title", new String(buffer, end - 125, 30).trim());
            id3v1.put("artist", new String(buffer, end - 95, 30).trim());
            id3v1.put("album", new String(buffer, end - 65, 30).trim());
            id3v1.put("year", new String(buffer, end - 35, 4).trim());
            id3v1.put("comment", new String(buffer, end - 31, 28).trim());
            id3v1.put("track", (buffer[end - 3] == 0) ? (buffer[end - 2] & 0xFF) : null);
            id3v1.put("genre", buffer[end - 1] & 0xFF);
            props.put("id3v1", id3v1);
        }
        // First frame header
        int headerOffset = props.containsKey("id3v2") ? 10 + (int) ((Map) props.get("id3v2")).size() * 10 : 0; // Approximate
        while (headerOffset < end - 4) {
            if ((buffer[headerOffset] & 0xFF) == 0xFF && (buffer[headerOffset + 1] & 0xE0) == 0xE0) break;
            headerOffset++;
        }
        if (headerOffset < end - 4) {
            byte[] header = Arrays.copyOfRange(buffer, headerOffset, headerOffset + 4);
            int versionBits = (header[1] >> 3) & 3;
            props.put("mpegVersion", versionBits == 3 ? 1.0 : versionBits == 2 ? 2.0 : 2.5);
            props.put("layer", 4 - ((header[1] >> 1) & 3));
            props.put("protection", (header[1] & 1) == 0);
            int bitrateIndex = header[2] >> 4;
            int[] bitrateTable = {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0};
            props.put("bitrate", bitrateTable[bitrateIndex]);
            int sampleIndex = (header[2] >> 2) & 3;
            int[] sampleTable = {44100, 48000, 32000, 0};
            props.put("samplingRate", sampleTable[sampleIndex]);
            props.put("padding", (header[2] >> 1) & 1);
            props.put("privateBit", header[2] & 1);
            int channelBits = header[3] >> 6;
            String[] channels = {"Stereo", "Joint stereo", "Dual channel", "Mono"};
            props.put("channelMode", channels[channelBits]);
            props.put("modeExtension", (header[3] >> 4) & 3);
            props.put("copyright", (header[3] >> 3) & 1);
            props.put("original", (header[3] >> 2) & 1);
            props.put("emphasis", header[3] & 3);
            // VBR Xing
            int xingOffset = headerOffset + ((Double) props.get("mpegVersion") == 1.0 && !props.get("channelMode").equals("Mono") ? 36 : 21);
            String xing = new String(buffer, xingOffset, 4);
            if (xing.equals("Xing") || xing.equals("Info")) {
                props.put("vbr", true);
                int flags = ByteBuffer.wrap(buffer, xingOffset + 4, 4).getInt();
                props.put("vbrFlags", flags);
                int numFrames = ByteBuffer.wrap(buffer, xingOffset + 8, 4).getInt();
                props.put("numFrames", numFrames);
            } else {
                props.put("vbr", false);
            }
        }
        props.put("fileSize", buffer.length);
        if (props.containsKey("numFrames")) {
            props.put("duration", ((Integer) props.get("numFrames")) * 1152.0 / ((Integer) props.get("samplingRate")));
        } else {
            props.put("duration", "Unknown (CBR)");
        }
        return props;
    }

    public void printProperties() {
        System.out.println(props);
    }

    public void write(String newFilepath, Map<String, String> modifications) throws IOException {
        byte[] newBuffer = buffer.clone();
        int end = newBuffer.length;
        if (props.containsKey("id3v1") && modifications.containsKey("title")) {
            String newTitle = modifications.get("title");
            byte[] titleBytes = newTitle.getBytes();
            System.arraycopy(titleBytes, 0, newBuffer, end - 125, Math.min(30, titleBytes.length));
        }
        // Add more mods as needed
        Files.write(Paths.get(newFilepath), newBuffer);
        System.out.println("Modified file saved to " + newFilepath);
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     MP3Parser parser = new MP3Parser("example.mp3");
    //     parser.printProperties();
    //     Map<String, String> mods = new HashMap<>();
    //     mods.put("title", "New Title");
    //     parser.write("modified.mp3", mods);
    // }
}
  1. JavaScript Class for MP3 Property Handling

This JavaScript class (Node.js compatible; requires fs module) opens an MP3 file, decodes/reads properties, prints to console, and writes (modifies e.g., ID3v1 title and saves).

const fs = require('fs');

class MP3Parser {
  constructor(filepath) {
    this.buffer = fs.readFileSync(filepath);
    this.props = this.parse();
  }

  parse() {
    const props = {};
    const buffer = this.buffer;
    // ID3v2
    if (buffer.toString('ascii', 0, 3) === 'ID3') {
      const major = buffer[3];
      const size = (buffer[6] << 21) | (buffer[7] << 14) | (buffer[8] << 7) | buffer[9];
      let offset = 10;
      props.id3v2 = {};
      while (offset < size + 10) {
        const frameId = buffer.toString('ascii', offset, offset + 4);
        if (!frameId.match(/^[A-Z0-9]{4}$/)) break;
        const frameSize = buffer.readUInt32BE(offset + 4);
        const frameData = buffer.toString('utf8', offset + 10, offset + 10 + frameSize).replace(/\0/g, '').trim();
        props.id3v2[frameId] = frameData;
        offset += 10 + frameSize;
      }
    }
    // ID3v1
    const end = buffer.length;
    if (buffer.toString('ascii', end - 128, end - 125) === 'TAG') {
      props.id3v1 = {
        title: buffer.toString('ascii', end - 125, end - 95).trim(),
        artist: buffer.toString('ascii', end - 95, end - 65).trim(),
        album: buffer.toString('ascii', end - 65, end - 35).trim(),
        year: buffer.toString('ascii', end - 35, end - 31).trim(),
        comment: buffer.toString('ascii', end - 31, end - 3).trim(),
        track: (buffer[end - 3] === 0) ? buffer[end - 2] : undefined,
        genre: buffer[end - 1]
      };
    }
    // First frame header
    let headerOffset = props.id3v2 ? 10 + size : 0;
    while (headerOffset < end - 4) {
      if (buffer[headerOffset] === 0xFF && (buffer[headerOffset + 1] & 0xE0) === 0xE0) break;
      headerOffset++;
    }
    if (headerOffset < end - 4) {
      const header = buffer.slice(headerOffset, headerOffset + 4);
      const versionBits = (header[1] >> 3) & 3;
      props.mpegVersion = versionBits === 3 ? 1 : versionBits === 2 ? 2 : 2.5;
      props.layer = 4 - ((header[1] >> 1) & 3);
      props.protection = (header[1] & 1) === 0;
      const bitrateIndex = header[2] >> 4;
      const bitrateTable = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0];
      props.bitrate = bitrateTable[bitrateIndex];
      const sampleIndex = (header[2] >> 2) & 3;
      const sampleTable = [44100, 48000, 32000, 0];
      props.samplingRate = sampleTable[sampleIndex];
      props.padding = (header[2] >> 1) & 1;
      props.privateBit = header[2] & 1;
      const channelBits = header[3] >> 6;
      props.channelMode = ['Stereo', 'Joint stereo', 'Dual channel', 'Mono'][channelBits];
      props.modeExtension = (header[3] >> 4) & 3;
      props.copyright = (header[3] >> 3) & 1;
      props.original = (header[3] >> 2) & 1;
      props.emphasis = header[3] & 3;
      // VBR Xing
      const xingOffset = headerOffset + (props.mpegVersion === 1 && props.channelMode !== 'Mono' ? 36 : 21);
      const xing = buffer.toString('ascii', xingOffset, xingOffset + 4);
      if (xing === 'Xing' || xing === 'Info') {
        props.vbr = true;
        props.vbrFlags = buffer.readUInt32BE(xingOffset + 4);
        props.numFrames = buffer.readUInt32BE(xingOffset + 8);
      } else {
        props.vbr = false;
      }
    }
    props.fileSize = buffer.length;
    props.duration = props.numFrames ? (props.numFrames * 1152 / props.samplingRate) : 'Unknown (CBR)';
    return props;
  }

  printProperties() {
    console.log(JSON.stringify(this.props, null, 2));
  }

  write(newFilepath, modifications = {}) {
    const newBuffer = Buffer.from(this.buffer);
    const end = newBuffer.length;
    if (this.props.id3v1 && modifications.title) {
      const newTitle = modifications.title.padEnd(30, ' ').slice(0, 30);
      newBuffer.write(newTitle, end - 125, 30, 'ascii');
    }
    // Add more mods as needed
    fs.writeFileSync(newFilepath, newBuffer);
    console.log(`Modified file saved to ${newFilepath}`);
  }
}

// Example usage:
// const parser = new MP3Parser('example.mp3');
// parser.printProperties();
// parser.write('modified.mp3', { title: 'New Title' });
  1. C++ Class for MP3 Property Handling

This C++ class uses std::ifstream for reading, parses properties, prints to console, and supports writing (modifying e.g., ID3v1 title and saving). Compile with g++.

#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <string>
#include <iomanip>
#include <cstring>

class MP3Parser {
private:
    std::vector<char> buffer;
    std::map<std::string, std::string> props; // Simplified to string for print

public:
    MP3Parser(const std::string& filepath) {
        std::ifstream file(filepath, std::ios::binary | std::ios::ate);
        auto size = file.tellg();
        buffer.resize(size);
        file.seekg(0);
        file.read(buffer.data(), size);
        parse();
    }

    void parse() {
        // ID3v2 (simplified)
        if (std::strncmp(buffer.data(), "ID3", 3) == 0) {
            int tag_size = ((buffer[6] & 0x7F) << 21) | ((buffer[7] & 0x7F) << 14) | ((buffer[8] & 0x7F) << 7) | (buffer[9] & 0x7F);
            int offset = 10;
            while (offset < tag_size + 10) {
                char frame_id[5] = {0};
                std::strncpy(frame_id, buffer.data() + offset, 4);
                if (!std::isalnum(frame_id[0]) || !std::isalnum(frame_id[1]) || !std::isalnum(frame_id[2]) || !std::isalnum(frame_id[3])) break;
                int frame_size = (buffer[offset + 4] << 24) | (buffer[offset + 5] << 16) | (buffer[offset + 6] << 8) | buffer[offset + 7];
                std::string frame_data(buffer.begin() + offset + 10, buffer.begin() + offset + 10 + frame_size);
                frame_data.erase(std::remove(frame_data.begin(), frame_data.end(), '\0'), frame_data.end());
                props["id3v2_" + std::string(frame_id)] = frame_data;
                offset += 10 + frame_size;
            }
        }
        // ID3v1
        size_t end = buffer.size();
        if (std::strncmp(buffer.data() + end - 128, "TAG", 3) == 0) {
            char temp[31] = {0};
            std::strncpy(temp, buffer.data() + end - 125, 30); props["id3v1_title"] = std::string(temp);
            std::strncpy(temp, buffer.data() + end - 95, 30); props["id3v1_artist"] = std::string(temp);
            std::strncpy(temp, buffer.data() + end - 65, 30); props["id3v1_album"] = std::string(temp);
            std::strncpy(temp, buffer.data() + end - 35, 4); props["id3v1_year"] = std::string(temp);
            std::strncpy(temp, buffer.data() + end - 31, 28); props["id3v1_comment"] = std::string(temp);
            if (buffer[end - 3] == 0) props["id3v1_track"] = std::to_string(static_cast<unsigned char>(buffer[end - 2]));
            props["id3v1_genre"] = std::to_string(static_cast<unsigned char>(buffer[end - 1]));
        }
        // First frame header
        size_t header_offset = props.count("id3v2_TIT2") > 0 ? 10 + 1024 : 0; // Approximate for ID3v2 size
        while (header_offset < end - 4) {
            if (static_cast<unsigned char>(buffer[header_offset]) == 0xFF && (static_cast<unsigned char>(buffer[header_offset + 1]) & 0xE0) == 0xE0) break;
            header_offset++;
        }
        if (header_offset < end - 4) {
            char header[4];
            std::memcpy(header, buffer.data() + header_offset, 4);
            int version_bits = (header[1] >> 3) & 3;
            props["mpeg_version"] = std::to_string(version_bits == 3 ? 1 : version_bits == 2 ? 2 : 2.5);
            props["layer"] = std::to_string(4 - ((header[1] >> 1) & 3));
            props["protection"] = (header[1] & 1) == 0 ? "yes" : "no";
            int bitrate_index = header[2] >> 4;
            int bitrate_table[16] = {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0};
            props["bitrate"] = std::to_string(bitrate_table[bitrate_index]);
            int sample_index = (header[2] >> 2) & 3;
            int sample_table[4] = {44100, 48000, 32000, 0};
            props["sampling_rate"] = std::to_string(sample_table[sample_index]);
            props["padding"] = std::to_string((header[2] >> 1) & 1);
            props["private_bit"] = std::to_string(header[2] & 1);
            int channel_bits = header[3] >> 6;
            std::string channels[4] = {"Stereo", "Joint stereo", "Dual channel", "Mono"};
            props["channel_mode"] = channels[channel_bits];
            props["mode_extension"] = std::to_string((header[3] >> 4) & 3);
            props["copyright"] = std::to_string((header[3] >> 3) & 1);
            props["original"] = std::to_string((header[3] >> 2) & 1);
            props["emphasis"] = std::to_string(header[3] & 3);
            // VBR Xing
            size_t xing_offset = header_offset + (std::stod(props["mpeg_version"]) == 1 && props["channel_mode"] != "Mono" ? 36 : 21);
            char xing[5] = {0};
            std::strncpy(xing, buffer.data() + xing_offset, 4);
            if (std::strcmp(xing, "Xing") == 0 || std::strcmp(xing, "Info") == 0) {
                props["vbr"] = "true";
                int flags = (buffer[xing_offset + 4] << 24) | (buffer[xing_offset + 5] << 16) | (buffer[xing_offset + 6] << 8) | buffer[xing_offset + 7];
                props["vbr_flags"] = std::to_string(flags);
                int num_frames = (buffer[xing_offset + 8] << 24) | (buffer[xing_offset + 9] << 16) | (buffer[xing_offset + 10] << 8) | buffer[xing_offset + 11];
                props["num_frames"] = std::to_string(num_frames);
            } else {
                props["vbr"] = "false";
            }
        }
        props["file_size"] = std::to_string(buffer.size());
        if (props.count("num_frames") > 0) {
            double duration = std::stoi(props["num_frames"]) * 1152.0 / std::stoi(props["sampling_rate"]);
            props["duration"] = std::to_string(duration);
        } else {
            props["duration"] = "Unknown (CBR)";
        }
    }

    void printProperties() {
        for (const auto& pair : props) {
            std::cout << pair.first << ": " << pair.second << std::endl;
        }
    }

    void write(const std::string& newFilepath, const std::map<std::string, std::string>& modifications) {
        std::vector<char> newBuffer = buffer;
        size_t end = newBuffer.size();
        auto it = modifications.find("title");
        if (props.count("id3v1_title") > 0 && it != modifications.end()) {
            std::string newTitle = it->second;
            newTitle.resize(30, ' ');
            std::memcpy(newBuffer.data() + end - 125, newTitle.c_str(), 30);
        }
        // Add more mods as needed
        std::ofstream out(newFilepath, std::ios::binary);
        out.write(newBuffer.data(), newBuffer.size());
        std::cout << "Modified file saved to " << newFilepath << std::endl;
    }
};

// Example usage:
// int main() {
//     MP3Parser parser("example.mp3");
//     parser.printProperties();
//     std::map<std::string, std::string> mods;
//     mods["title"] = "New Title";
//     parser.write("modified.mp3", mods);
//     return 0;
// }