Login
1 branch 0 tags
Ben (Desktop/Arch) Added clang-format 2f88780 1 month ago 66 Commits
moon / src / metadata / ogg.c
#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;
}