text/plain
•
6.04 KB
•
253 lines
#include "../audio_metadata.h"
#include "helper.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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);
}
// Estimate average bitrate from file size and duration (enables seeking)
if (meta->duration_ms > 0) {
meta->bitrate =
(uint32_t)((uint64_t)file_size * 8000 / meta->duration_ms);
}
meta->valid = true;
return true;
}