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

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

// Read little-endian uint16
static uint16_t read_le16(const uint8_t *buf) {
    return (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
}

// Read little-endian uint32
static uint32_t read_le32(const uint8_t *buf) {
    return (uint32_t)buf[0] | ((uint32_t)buf[1] << 8) |
           ((uint32_t)buf[2] << 16) | ((uint32_t)buf[3] << 24);
}

// Read little-endian uint64
static uint64_t read_le64(const uint8_t *buf) {
    return (uint64_t)read_le32(buf) | ((uint64_t)read_le32(buf + 4) << 32);
}

// Parse Vorbis comment data for tags
static void parse_vorbis_tags(const uint8_t *buf, uint32_t size, audio_metadata_t *meta) {
    uint32_t pos = 0;

    if (pos + 4 > size) return;
    uint32_t vendor_len = read_le32(&buf[pos]);
    pos += 4 + vendor_len;

    if (pos + 4 > size) return;
    uint32_t num_comments = read_le32(&buf[pos]);
    pos += 4;

    for (uint32_t i = 0; i < num_comments && pos + 4 <= size; i++) {
        uint32_t comment_len = read_le32(&buf[pos]);
        pos += 4;

        if (pos + comment_len > size) break;

        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;

            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;
    }
}

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

    // Get file size
    fseek(f, 0, SEEK_END);
    long file_size = ftell(f);
    fseek(f, 0, SEEK_SET);

    if (file_size < 28) {
        fclose(f);
        return false;
    }

    // Read first OGG page header
    uint8_t first_header[27];
    if (fread(first_header, 1, 27, f) != 27 || memcmp(first_header, "OggS", 4) != 0) {
        fclose(f);
        return false;
    }

    uint8_t num_segments = first_header[26];
    uint8_t segments[255];
    if (fread(segments, 1, num_segments, f) != num_segments) {
        fclose(f);
        return false;
    }

    uint32_t body_size = 0;
    for (int i = 0; i < num_segments; i++) {
        body_size += segments[i];
    }

    // Read first page body to detect codec
    if (body_size > 4096) {
        fclose(f);
        return false;
    }

    uint8_t *body = malloc(body_size);
    if (!body) {
        fclose(f);
        return false;
    }

    if (fread(body, 1, body_size, f) != body_size) {
        free(body);
        fclose(f);
        return false;
    }

    uint32_t sample_rate = 0;
    uint16_t pre_skip = 0;
    bool is_opus = false;

    // Detect codec from first packet
    if (body_size >= 30 && memcmp(body, "\x01vorbis", 7) == 0) {
        // Vorbis identification header
        meta->channels = body[11];
        sample_rate = read_le32(&body[12]);
    } else if (body_size >= 19 && memcmp(body, "OpusHead", 8) == 0) {
        // Opus header
        is_opus = true;
        meta->channels = body[9];
        pre_skip = read_le16(&body[10]);
        sample_rate = 48000; // Opus always uses 48kHz for granule position
    } else {
        free(body);
        fclose(f);
        return false;
    }

    free(body);
    meta->sample_rate = sample_rate;

    // Read second page (comment header) for tags
    uint8_t comment_header[27];
    if (fread(comment_header, 1, 27, f) == 27 && memcmp(comment_header, "OggS", 4) == 0) {
        uint8_t cseg = comment_header[26];
        uint8_t csegs[255];
        if (fread(csegs, 1, cseg, f) == cseg) {
            uint32_t cbody_size = 0;
            for (int i = 0; i < cseg; i++) cbody_size += csegs[i];

            if (cbody_size > 0 && cbody_size <= 256 * 1024) {
                uint8_t *cbody = malloc(cbody_size);
                if (cbody && fread(cbody, 1, cbody_size, f) == cbody_size) {
                    // Skip codec-specific prefix
                    uint32_t offset = 0;
                    if (!is_opus && cbody_size > 7 && memcmp(cbody, "\x03vorbis", 7) == 0) {
                        offset = 7;
                    } else if (is_opus && cbody_size > 8 && memcmp(cbody, "OpusTags", 8) == 0) {
                        offset = 8;
                    }
                    if (offset > 0) {
                        parse_vorbis_tags(cbody + offset, cbody_size - offset, meta);
                    }
                }
                free(cbody);
            }
        }
    }

    // Seek near end of file and scan for last OGG page to get final granule position
    // Read last 64KB (or whole file if smaller)
    long scan_size = 65536;
    if (scan_size > file_size) scan_size = file_size;

    long scan_start = file_size - scan_size;
    fseek(f, scan_start, SEEK_SET);

    uint8_t *scan_buf = malloc(scan_size);
    if (!scan_buf) {
        fclose(f);
        return false;
    }

    long bytes_read = fread(scan_buf, 1, scan_size, f);
    fclose(f);

    // Scan backwards for last "OggS" marker
    uint64_t last_granule = 0;
    bool found_last = false;

    for (long i = bytes_read - 14; i >= 0; i--) {
        if (scan_buf[i] == 'O' && scan_buf[i+1] == 'g' &&
            scan_buf[i+2] == 'g' && scan_buf[i+3] == 'S') {
            last_granule = read_le64(&scan_buf[i + 6]);
            // Skip pages with granule -1 (header pages)
            if (last_granule != UINT64_MAX) {
                found_last = true;
                break;
            }
        }
    }

    free(scan_buf);

    if (!found_last || sample_rate == 0) return false;

    if (is_opus) {
        if (last_granule > pre_skip) {
            meta->duration_ms = (uint32_t)((last_granule - pre_skip) * 1000 / sample_rate);
        }
    } else {
        meta->duration_ms = (uint32_t)(last_granule * 1000 / sample_rate);
    }

    meta->valid = true;
    return true;
}