Login
1 branch 0 tags
Ben (Desktop/Arch) Software controlled display brightness c2671dd 1 month ago 67 Commits
moon / src / metadata / m4a.c
#include "../audio_metadata.h"
#include "helper.h"

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

// ============================================================================
// M4A/MP4 Parser
// ============================================================================

// Read atom header, return atom size (0 on error)
static uint64_t read_atom_header(FILE* f, char* type) {
	uint8_t header[8];
	if (fread(header, 1, 8, f) != 8) {
		return 0;
	}

	uint32_t size = read_be32(header);
	memcpy(type, &header[4], 4);
	type[4] = '\0';

	if (size == 1) {
		// Extended size (64-bit)
		uint8_t ext[8];
		if (fread(ext, 1, 8, f) != 8) {
			return 0;
		}
		return ((uint64_t)read_be32(ext) << 32) | read_be32(&ext[4]);
	}

	return size;
}

// Parse mvhd atom for duration
static bool parse_mvhd(FILE* f, uint32_t size, audio_metadata_t* meta) {
	if (size < 20) {
		return false;
	}

	uint8_t buf[32];
	if (fread(buf, 1, 32, f) != 32) {
		return false;
	}

	uint8_t version = buf[0];
	uint32_t timescale, duration;

	if (version == 0) {
		// 32-bit values
		timescale = read_be32(&buf[12]);
		duration = read_be32(&buf[16]);
	} else {
		// 64-bit values - need to read more
		fseek(f, -32, SEEK_CUR);
		uint8_t buf64[40];
		if (fread(buf64, 1, 40, f) != 40) {
			return false;
		}
		timescale = read_be32(&buf64[20]);
		duration = read_be32(&buf64[28]);  // Only use lower 32 bits
	}

	if (timescale > 0) {
		meta->duration_ms = (uint32_t)((uint64_t)duration * 1000 / timescale);
	}

	return true;
}

// Parse iTunes metadata atom
static void parse_ilst_data(FILE* f,
                            uint32_t size,
                            char* dest,
                            size_t dest_size) {
	// Skip to 'data' atom
	long end = ftell(f) + size - 8;

	while (ftell(f) < end) {
		char type[5];
		uint64_t atom_size = read_atom_header(f, type);
		if (atom_size == 0) {
			break;
		}

		if (strcmp(type, "data") == 0) {
			// Skip version and flags (4 bytes) and null (4 bytes)
			fseek(f, 8, SEEK_CUR);

			size_t text_len = atom_size - 16;  // 8 header + 8 skipped
			if (text_len > 0 && text_len < 1024) {
				if (text_len >= dest_size) {
					text_len = dest_size - 1;
				}
				if (fread(dest, 1, text_len, f) == text_len) {
					dest[text_len] = '\0';
				}
			}
			return;
		}

		// Skip this atom
		fseek(f, atom_size - 8, SEEK_CUR);
	}
}

// Parse ilst (iTunes metadata list)
static void parse_ilst(FILE* f, uint32_t size, audio_metadata_t* meta) {
	long end = ftell(f) + size - 8;

	while (ftell(f) < end) {
		char type[5];
		uint64_t atom_size = read_atom_header(f, type);
		if (atom_size == 0) {
			break;
		}

		// iTunes uses special atoms: \251nam (title), \251ART (artist), \251alb
		// (album)
		if (strcmp(type, "\251nam") == 0) {
			parse_ilst_data(f, atom_size, meta->title, sizeof(meta->title));
		} else if (strcmp(type, "\251ART") == 0) {
			parse_ilst_data(f, atom_size, meta->artist, sizeof(meta->artist));
		} else if (strcmp(type, "\251alb") == 0) {
			parse_ilst_data(f, atom_size, meta->album, sizeof(meta->album));
		} else {
			fseek(f, atom_size - 8, SEEK_CUR);
		}
	}
}

// Recursively search for atoms in moov
static void parse_moov_recursive(FILE* f, long end, audio_metadata_t* meta) {
	while (ftell(f) < end) {
		long atom_start = ftell(f);
		char type[5];
		uint64_t atom_size = read_atom_header(f, type);

		if (atom_size == 0 || atom_size > (uint64_t)(end - atom_start)) {
			break;
		}

		if (strcmp(type, "mvhd") == 0) {
			parse_mvhd(f, atom_size, meta);
		} else if (strcmp(type, "udta") == 0 || strcmp(type, "meta") == 0) {
			// meta has 4 extra bytes (version/flags)
			if (strcmp(type, "meta") == 0) {
				fseek(f, 4, SEEK_CUR);
			}
			parse_moov_recursive(f, atom_start + atom_size, meta);
		} else if (strcmp(type, "ilst") == 0) {
			parse_ilst(f, atom_size, meta);
		}

		fseek(f, atom_start + atom_size, SEEK_SET);
	}
}

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

	fseek(f, 0, SEEK_END);
	long file_size = ftell(f);
	fseek(f, 0, SEEK_SET);

	// Look for moov atom
	while (ftell(f) < file_size) {
		long atom_start = ftell(f);
		char type[5];
		uint64_t atom_size = read_atom_header(f, type);

		if (atom_size == 0) {
			break;
		}

		if (strcmp(type, "moov") == 0) {
			parse_moov_recursive(f, atom_start + atom_size, meta);
			meta->valid = (meta->duration_ms > 0);
			break;
		}

		fseek(f, atom_start + atom_size, SEEK_SET);
	}

	fclose(f);
	return meta->valid;
}