Login
1 branch 0 tags
Ben (Desktop/Arch) Reorganized things 6d669c1 1 month ago 48 Commits
moon / src / metadata / m4a.c
#include "../audio_metadata.h"
#include "helper.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

// ============================================================================
// M4A/MP4 Parser
// ============================================================================

// Read atom header, return atom size (0 on error)
static uint64_t read_atom_header(FILE *f, char *type) {
    uint8_t header[8];
    if (fread(header, 1, 8, f) != 8) return 0;

    uint32_t size = read_be32(header);
    memcpy(type, &header[4], 4);
    type[4] = '\0';

    if (size == 1) {
        // Extended size (64-bit)
        uint8_t ext[8];
        if (fread(ext, 1, 8, f) != 8) return 0;
        return ((uint64_t)read_be32(ext) << 32) | read_be32(&ext[4]);
    }

    return size;
}

// Parse mvhd atom for duration
static bool parse_mvhd(FILE *f, uint32_t size, audio_metadata_t *meta) {
    if (size < 20) return false;

    uint8_t buf[32];
    if (fread(buf, 1, 32, f) != 32) return false;

    uint8_t version = buf[0];
    uint32_t timescale, duration;

    if (version == 0) {
        // 32-bit values
        timescale = read_be32(&buf[12]);
        duration = read_be32(&buf[16]);
    } else {
        // 64-bit values - need to read more
        fseek(f, -32, SEEK_CUR);
        uint8_t buf64[40];
        if (fread(buf64, 1, 40, f) != 40) return false;
        timescale = read_be32(&buf64[20]);
        duration = read_be32(&buf64[28]);  // Only use lower 32 bits
    }

    if (timescale > 0) {
        meta->duration_ms = (uint32_t)((uint64_t)duration * 1000 / timescale);
    }

    return true;
}

// Parse iTunes metadata atom
static void parse_ilst_data(FILE *f, uint32_t size, char *dest, size_t dest_size) {
    // Skip to 'data' atom
    long end = ftell(f) + size - 8;

    while (ftell(f) < end) {
        char type[5];
        uint64_t atom_size = read_atom_header(f, type);
        if (atom_size == 0) break;

        if (strcmp(type, "data") == 0) {
            // Skip version and flags (4 bytes) and null (4 bytes)
            fseek(f, 8, SEEK_CUR);

            size_t text_len = atom_size - 16;  // 8 header + 8 skipped
            if (text_len > 0 && text_len < 1024) {
                if (text_len >= dest_size) text_len = dest_size - 1;
                if (fread(dest, 1, text_len, f) == text_len) {
                    dest[text_len] = '\0';
                }
            }
            return;
        }

        // Skip this atom
        fseek(f, atom_size - 8, SEEK_CUR);
    }
}

// Parse ilst (iTunes metadata list)
static void parse_ilst(FILE *f, uint32_t size, audio_metadata_t *meta) {
    long end = ftell(f) + size - 8;

    while (ftell(f) < end) {
        char type[5];
        uint64_t atom_size = read_atom_header(f, type);
        if (atom_size == 0) break;

        // iTunes uses special atoms: \251nam (title), \251ART (artist), \251alb (album)
        if (strcmp(type, "\251nam") == 0) {
            parse_ilst_data(f, atom_size, meta->title, sizeof(meta->title));
        } else if (strcmp(type, "\251ART") == 0) {
            parse_ilst_data(f, atom_size, meta->artist, sizeof(meta->artist));
        } else if (strcmp(type, "\251alb") == 0) {
            parse_ilst_data(f, atom_size, meta->album, sizeof(meta->album));
        } else {
            fseek(f, atom_size - 8, SEEK_CUR);
        }
    }
}

// Recursively search for atoms in moov
static void parse_moov_recursive(FILE *f, long end, audio_metadata_t *meta) {
    while (ftell(f) < end) {
        long atom_start = ftell(f);
        char type[5];
        uint64_t atom_size = read_atom_header(f, type);

        if (atom_size == 0 || atom_size > (uint64_t)(end - atom_start)) break;

        if (strcmp(type, "mvhd") == 0) {
            parse_mvhd(f, atom_size, meta);
        } else if (strcmp(type, "udta") == 0 || strcmp(type, "meta") == 0) {
            // meta has 4 extra bytes (version/flags)
            if (strcmp(type, "meta") == 0) {
                fseek(f, 4, SEEK_CUR);
            }
            parse_moov_recursive(f, atom_start + atom_size, meta);
        } else if (strcmp(type, "ilst") == 0) {
            parse_ilst(f, atom_size, meta);
        }

        fseek(f, atom_start + atom_size, SEEK_SET);
    }
}

bool parse_m4a(const char *path, audio_metadata_t *meta) {
    FILE *f = fopen(path, "rb");
    if (!f) return false;

    fseek(f, 0, SEEK_END);
    long file_size = ftell(f);
    fseek(f, 0, SEEK_SET);

    // Look for moov atom
    while (ftell(f) < file_size) {
        long atom_start = ftell(f);
        char type[5];
        uint64_t atom_size = read_atom_header(f, type);

        if (atom_size == 0) break;

        if (strcmp(type, "moov") == 0) {
            parse_moov_recursive(f, atom_start + atom_size, meta);
            meta->valid = (meta->duration_ms > 0);
            break;
        }

        fseek(f, atom_start + atom_size, SEEK_SET);
    }

    fclose(f);
    return meta->valid;
}