Login
1 branch 0 tags
Ben (Desktop/Arch) Limited podcast episode list to 16 entries b36bce8 29 days ago 80 Commits
moon / src / metadata / flac.c
#include "../audio_metadata.h"
#include "helper.h"

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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;
}