Login
1 branch 0 tags
Ben (Desktop/Arch) More reliable metadata 91cec1c 1 month ago 26 Commits
moon / firmware / src / metadata / flac.c
#include "../audio_metadata.h"
#include "helper.h"

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

// Parse a Vorbis comment block for title/artist/album tags
static void parse_vorbis_comment(FILE *f, uint32_t block_size, audio_metadata_t *meta) {
    if (block_size < 8 || block_size > 1024 * 1024) return;

    uint8_t *buf = malloc(block_size);
    if (!buf) return;

    if (fread(buf, 1, block_size, f) != block_size) {
        free(buf);
        return;
    }

    // Vorbis comment format:
    // 4 bytes: vendor string length (LE)
    // N bytes: vendor string
    // 4 bytes: number of comments (LE)
    // For each comment:
    //   4 bytes: comment length (LE)
    //   N bytes: "KEY=value" UTF-8

    uint32_t pos = 0;
    if (pos + 4 > block_size) { free(buf); return; }

    uint32_t vendor_len = (uint32_t)buf[pos] | ((uint32_t)buf[pos+1] << 8) |
                          ((uint32_t)buf[pos+2] << 16) | ((uint32_t)buf[pos+3] << 24);
    pos += 4 + vendor_len;

    if (pos + 4 > block_size) { free(buf); return; }

    uint32_t num_comments = (uint32_t)buf[pos] | ((uint32_t)buf[pos+1] << 8) |
                            ((uint32_t)buf[pos+2] << 16) | ((uint32_t)buf[pos+3] << 24);
    pos += 4;

    for (uint32_t i = 0; i < num_comments && pos + 4 <= block_size; i++) {
        uint32_t comment_len = (uint32_t)buf[pos] | ((uint32_t)buf[pos+1] << 8) |
                               ((uint32_t)buf[pos+2] << 16) | ((uint32_t)buf[pos+3] << 24);
        pos += 4;

        if (pos + comment_len > block_size) break;

        // Find '=' separator
        const char *comment = (const char *)&buf[pos];
        const char *eq = memchr(comment, '=', comment_len);
        if (eq) {
            size_t key_len = (size_t)(eq - comment);
            const char *value = eq + 1;
            size_t value_len = comment_len - key_len - 1;

            // Case-insensitive key match
            if (key_len == 5 && strncasecmp(comment, "TITLE", 5) == 0) {
                if (value_len >= sizeof(meta->title)) value_len = sizeof(meta->title) - 1;
                memcpy(meta->title, value, value_len);
                meta->title[value_len] = '\0';
            } else if (key_len == 6 && strncasecmp(comment, "ARTIST", 6) == 0) {
                if (value_len >= sizeof(meta->artist)) value_len = sizeof(meta->artist) - 1;
                memcpy(meta->artist, value, value_len);
                meta->artist[value_len] = '\0';
            } else if (key_len == 5 && strncasecmp(comment, "ALBUM", 5) == 0) {
                if (value_len >= sizeof(meta->album)) value_len = sizeof(meta->album) - 1;
                memcpy(meta->album, value, value_len);
                meta->album[value_len] = '\0';
            }
        }

        pos += comment_len;
    }

    free(buf);
}

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

    // Verify "fLaC" magic
    uint8_t magic[4];
    if (fread(magic, 1, 4, f) != 4 || memcmp(magic, "fLaC", 4) != 0) {
        fclose(f);
        return false;
    }

    // Read metadata blocks
    bool last_block = false;
    bool got_streaminfo = false;

    while (!last_block) {
        // Block header: 1 byte (last flag + type), 3 bytes (size)
        uint8_t block_header[4];
        if (fread(block_header, 1, 4, f) != 4) break;

        last_block = (block_header[0] & 0x80) != 0;
        uint8_t block_type = block_header[0] & 0x7F;
        uint32_t block_size = ((uint32_t)block_header[1] << 16) |
                              ((uint32_t)block_header[2] << 8) |
                              block_header[3];

        if (block_type == 0) {
            // STREAMINFO block (always 34 bytes)
            if (block_size < 34) break;
            uint8_t si[34];
            if (fread(si, 1, 34, f) != 34) break;

            // Byte layout of STREAMINFO:
            // 0-1:   min block size
            // 2-3:   max block size
            // 4-6:   min frame size
            // 7-9:   max frame size
            // 10-13: sample rate (20 bits), channels-1 (3 bits), bps-1 (5 bits), total samples high (4 bits)
            // 14-17: total samples low (32 bits)
            // 18-33: MD5

            uint32_t sample_rate = ((uint32_t)si[10] << 12) |
                                   ((uint32_t)si[11] << 4) |
                                   ((uint32_t)si[12] >> 4);
            meta->channels = ((si[12] >> 1) & 0x07) + 1;
            // total_samples is 36 bits: 4 bits from byte 13, 32 bits from bytes 14-17
            uint64_t total_samples = ((uint64_t)(si[13] & 0x0F) << 32) |
                                     ((uint64_t)si[14] << 24) |
                                     ((uint64_t)si[15] << 16) |
                                     ((uint64_t)si[16] << 8) |
                                     si[17];

            meta->sample_rate = sample_rate;
            if (sample_rate > 0) {
                meta->duration_ms = (uint32_t)(total_samples * 1000 / sample_rate);
            }

            // Skip remaining if block is larger than 34
            if (block_size > 34) {
                fseek(f, block_size - 34, SEEK_CUR);
            }
            got_streaminfo = true;
        } else if (block_type == 4) {
            // VORBIS_COMMENT block
            parse_vorbis_comment(f, block_size, meta);
        } else {
            // Skip unknown block
            fseek(f, block_size, SEEK_CUR);
        }
    }

    fclose(f);

    if (!got_streaminfo) return false;

    meta->valid = true;
    return true;
}