Files
mice-3ds/source/metadata.c
angel 285351e982
Some checks failed
Build (3DS) / build (push) Failing after 1m13s
YAY
2025-12-06 23:47:15 +00:00

657 lines
16 KiB
C

#include <3ds.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "metadata.h"
#include "file.h"
#include "all.h"
/* Internal helper functions */
static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata);
static int extractId3v1Metadata(FILE* fp, struct metadata_t* metadata);
static int extractVorbisComment(FILE* fp, struct metadata_t* metadata);
static int extractFlacMetadata(FILE* fp, struct metadata_t* metadata);
static void trimWhitespace(char* str);
static void parseFilename(const char* file, struct metadata_t* metadata);
/**
* Extract metadata from an audio file
*/
int extractMetadata(const char* file, struct metadata_t* metadata)
{
FILE* fp;
enum file_types fileType;
if(!file || !metadata)
return -1;
/* Clear metadata structure */
clearMetadata(metadata);
fileType = getFileType(file);
fp = fopen(file, "rb");
if(!fp)
return -1;
switch(fileType)
{
case FILE_TYPE_MP3:
/* Try ID3v2 first, then ID3v1 */
if(extractId3v2Metadata(fp, metadata) != 0)
extractId3v1Metadata(fp, metadata);
break;
case FILE_TYPE_VORBIS:
case FILE_TYPE_OPUS:
extractVorbisComment(fp, metadata);
break;
case FILE_TYPE_FLAC:
extractFlacMetadata(fp, metadata);
break;
case FILE_TYPE_WAV:
case FILE_TYPE_SID:
default:
/* No metadata support for these formats yet */
break;
}
fclose(fp);
/* If no metadata was found, try to parse filename */
if(!metadata->title[0] && !metadata->artist[0] && !metadata->album[0])
{
parseFilename(file, metadata);
}
return 0;
}
/**
* Clear metadata structure and free any allocated memory
*/
void clearMetadata(struct metadata_t* metadata)
{
if(!metadata)
return;
memset(metadata->title, 0, METADATA_TITLE_MAX);
memset(metadata->artist, 0, METADATA_ARTIST_MAX);
memset(metadata->album, 0, METADATA_ALBUM_MAX);
if(metadata->albumArt)
{
free(metadata->albumArt);
metadata->albumArt = NULL;
}
metadata->albumArtSize = 0;
metadata->albumArtWidth = 0;
metadata->albumArtHeight = 0;
metadata->hasAlbumArt = false;
}
/**
* Display metadata on the top screen
*/
void displayMetadata(struct metadata_t* metadata, const char* filename)
{
if(!metadata || !filename)
return;
/* Clear the top screen info area */
consoleClear();
/* Extract just the filename without path and extension for fallback */
const char* basename = strrchr(filename, '/');
if(!basename)
basename = filename;
else
basename++; /* Skip the '/' */
/* Remove file extension for display */
char displayName[64];
strncpy(displayName, basename, sizeof(displayName) - 1);
displayName[sizeof(displayName) - 1] = '\0';
char* dot = strrchr(displayName, '.');
if(dot) *dot = '\0';
/* Display song title */
if(metadata->title[0])
printf("%.47s\n", metadata->title);
else
printf("%.47s\n", displayName);
/* Display album */
if(metadata->album[0])
printf("%.47s\n", metadata->album);
else
printf("Unknown Album\n");
/* Display artist with album art indicator */
if(metadata->artist[0])
{
printf("%.45s", metadata->artist);
if(metadata->hasAlbumArt)
printf(" 🖼️");
printf("\n");
}
else
{
printf("Unknown Artist");
if(metadata->hasAlbumArt)
printf(" 🖼️");
printf("\n");
}
}
/**
* Display album art on top screen if available
*/
void displayAlbumArt(struct metadata_t* metadata)
{
if(!metadata || !metadata->hasAlbumArt || !metadata->albumArt)
return;
/* For now, just indicate that album art is available */
/* Full implementation would require image decoding and display */
printf("🖼️ Album Art: %dx%d\n", metadata->albumArtWidth, metadata->albumArtHeight);
}
/**
* Extract ID3v2 metadata from MP3 file
*/
static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata)
{
char header[10];
uint32_t tagSize;
uint8_t major, minor, flags;
if(!fp || !metadata)
return -1;
fseek(fp, 0, SEEK_SET);
if(fread(header, 1, 10, fp) != 10)
return -1;
/* Check ID3v2 header "ID3" */
if(memcmp(header, "ID3", 3) != 0)
return -1;
major = header[3];
minor = header[4];
flags = header[5];
/* Calculate tag size (synchsafe integer) */
tagSize = ((header[6] & 0x7F) << 21) |
((header[7] & 0x7F) << 14) |
((header[8] & 0x7F) << 7) |
(header[9] & 0x7F);
/* Skip extended header if present */
if(flags & 0x40)
{
uint32_t extSize;
if(fread(&extSize, 4, 1, fp) != 1)
return -1;
/* Convert from big-endian */
extSize = __builtin_bswap32(extSize);
fseek(fp, extSize - 4, SEEK_CUR);
}
long tagStart = ftell(fp);
long tagEnd = tagStart + tagSize;
/* Parse frames */
while(ftell(fp) < tagEnd - 10)
{
char frameId[5] = {0};
uint32_t frameSize;
uint16_t frameFlags;
if(fread(frameId, 1, 4, fp) != 4)
break;
/* Check for padding */
if(frameId[0] == 0)
break;
if(fread(&frameSize, 4, 1, fp) != 1)
break;
frameSize = __builtin_bswap32(frameSize);
if(fread(&frameFlags, 2, 1, fp) != 1)
break;
/* Handle text frames */
if(strncmp(frameId, "TIT2", 4) == 0 || /* Title */
strncmp(frameId, "TPE1", 4) == 0 || /* Artist */
strncmp(frameId, "TALB", 4) == 0) /* Album */
{
char* frameData = malloc(frameSize + 1);
if(!frameData)
{
fseek(fp, frameSize, SEEK_CUR);
continue;
}
if(fread(frameData, 1, frameSize, fp) == frameSize)
{
frameData[frameSize] = 0;
/* Skip text encoding byte */
char* text = frameData + 1;
int textLen = frameSize - 1;
/* Copy to appropriate field */
char* dest = NULL;
size_t maxLen = 0;
if(strncmp(frameId, "TIT2", 4) == 0)
{
dest = metadata->title;
maxLen = METADATA_TITLE_MAX - 1;
}
else if(strncmp(frameId, "TPE1", 4) == 0)
{
dest = metadata->artist;
maxLen = METADATA_ARTIST_MAX - 1;
}
else if(strncmp(frameId, "TALB", 4) == 0)
{
dest = metadata->album;
maxLen = METADATA_ALBUM_MAX - 1;
}
if(dest)
{
strncpy(dest, text, maxLen);
dest[maxLen] = 0;
trimWhitespace(dest);
}
}
free(frameData);
}
else if(strncmp(frameId, "APIC", 4) == 0) /* Attached Picture */
{
/* Basic album art detection - just store size info for now */
if(frameSize > 10 && !metadata->hasAlbumArt)
{
metadata->hasAlbumArt = true;
metadata->albumArtSize = frameSize;
/* Estimate dimensions - actual implementation would decode image */
metadata->albumArtWidth = 300; /* Common album art size */
metadata->albumArtHeight = 300;
}
fseek(fp, frameSize, SEEK_CUR);
}
else
{
/* Skip unknown frame */
fseek(fp, frameSize, SEEK_CUR);
}
}
return 0;
}
/**
* Extract ID3v1 metadata from MP3 file
*/
static int extractId3v1Metadata(FILE* fp, struct metadata_t* metadata)
{
char tag[128];
if(!fp || !metadata)
return -1;
/* Seek to last 128 bytes */
fseek(fp, -128, SEEK_END);
if(fread(tag, 1, 128, fp) != 128)
return -1;
/* Check for "TAG" signature */
if(memcmp(tag, "TAG", 3) != 0)
return -1;
/* Extract fields */
if(!metadata->title[0])
{
strncpy(metadata->title, tag + 3, 30);
metadata->title[30] = 0;
trimWhitespace(metadata->title);
}
if(!metadata->artist[0])
{
strncpy(metadata->artist, tag + 33, 30);
metadata->artist[30] = 0;
trimWhitespace(metadata->artist);
}
if(!metadata->album[0])
{
strncpy(metadata->album, tag + 63, 30);
metadata->album[30] = 0;
trimWhitespace(metadata->album);
}
return 0;
}
/**
* Extract Vorbis comment metadata (for OGG Vorbis/Opus)
*/
static int extractVorbisComment(FILE* fp, struct metadata_t* metadata)
{
/* This is a simplified implementation for Vorbis comments.
* A full implementation would require parsing the OGG container structure.
* For now, we'll try to find common Vorbis comment fields by searching for them.
*/
char buffer[4096];
size_t bytesRead;
if(!fp || !metadata)
return -1;
/* Reset to beginning */
fseek(fp, 0, SEEK_SET);
/* Read and search for Vorbis comment patterns */
while((bytesRead = fread(buffer, 1, sizeof(buffer) - 1, fp)) > 0)
{
buffer[bytesRead] = '\0';
/* Look for TITLE= */
char* title = strstr(buffer, "TITLE=");
if(title && !metadata->title[0])
{
title += 6; /* Skip "TITLE=" */
char* end = strchr(title, '\1'); /* Vorbis comments end with \1 or \0 */
if(!end) end = strchr(title, '\0');
if(end)
{
size_t len = end - title;
if(len >= METADATA_TITLE_MAX) len = METADATA_TITLE_MAX - 1;
strncpy(metadata->title, title, len);
metadata->title[len] = '\0';
trimWhitespace(metadata->title);
}
}
/* Look for ARTIST= or ALBUMARTIST= */
char* artist = strstr(buffer, "ARTIST=");
if(!artist)
artist = strstr(buffer, "ALBUMARTIST=");
if(artist && !metadata->artist[0])
{
if(strncmp(artist, "ALBUMARTIST=", 12) == 0)
artist += 12; /* Skip "ALBUMARTIST=" */
else
artist += 7; /* Skip "ARTIST=" */
char* end = strchr(artist, '\1');
if(!end) end = strchr(artist, '\0');
if(end)
{
size_t len = end - artist;
if(len >= METADATA_ARTIST_MAX) len = METADATA_ARTIST_MAX - 1;
strncpy(metadata->artist, artist, len);
metadata->artist[len] = '\0';
trimWhitespace(metadata->artist);
}
}
/* Look for ALBUM= */
char* album = strstr(buffer, "ALBUM=");
if(album && !metadata->album[0])
{
album += 6; /* Skip "ALBUM=" */
char* end = strchr(album, '\1');
if(!end) end = strchr(album, '\0');
if(end)
{
size_t len = end - album;
if(len >= METADATA_ALBUM_MAX) len = METADATA_ALBUM_MAX - 1;
strncpy(metadata->album, album, len);
metadata->album[len] = '\0';
trimWhitespace(metadata->album);
}
}
}
return 0;
}
/**
* Extract FLAC metadata
*/
static int extractFlacMetadata(FILE* fp, struct metadata_t* metadata)
{
char header[4];
if(!fp || !metadata)
return -1;
/* Check for FLAC signature */
fseek(fp, 0, SEEK_SET);
if(fread(header, 1, 4, fp) != 4 || memcmp(header, "fLaC", 4) != 0)
return -1;
/* FLAC metadata blocks follow the signature */
while(1)
{
uint8_t blockHeader[4];
uint32_t blockSize;
bool isLast;
uint8_t blockType;
if(fread(blockHeader, 1, 4, fp) != 4)
break;
isLast = (blockHeader[0] & 0x80) != 0;
blockType = blockHeader[0] & 0x7F;
blockSize = (blockHeader[1] << 16) | (blockHeader[2] << 8) | blockHeader[3];
if(blockType == 4) /* VORBIS_COMMENT */
{
/* FLAC uses Vorbis comments for metadata */
/* This is a simplified implementation */
char* commentData = malloc(blockSize);
if(commentData)
{
if(fread(commentData, 1, blockSize, fp) == blockSize)
{
/* Skip vendor string length (4 bytes) and vendor string */
if(blockSize > 4)
{
uint32_t vendorLength = commentData[0] | (commentData[1] << 8) |
(commentData[2] << 16) | (commentData[3] << 24);
if(vendorLength + 8 < blockSize) /* 4 bytes vendor len + vendor + 4 bytes comment count */
{
uint32_t commentCount = commentData[4 + vendorLength] |
(commentData[5 + vendorLength] << 8) |
(commentData[6 + vendorLength] << 16) |
(commentData[7 + vendorLength] << 24);
uint32_t offset = 8 + vendorLength;
for(uint32_t i = 0; i < commentCount && offset < blockSize; i++)
{
if(offset + 4 > blockSize) break;
uint32_t commentLength = commentData[offset] | (commentData[offset + 1] << 8) |
(commentData[offset + 2] << 16) | (commentData[offset + 3] << 24);
offset += 4;
if(offset + commentLength > blockSize) break;
/* Parse comment */
char* comment = commentData + offset;
if(commentLength > 0 && commentLength < 1000) /* Reasonable size check */
{
char tempComment[1001];
strncpy(tempComment, comment, commentLength);
tempComment[commentLength] = '\0';
if(strncmp(tempComment, "TITLE=", 6) == 0 && !metadata->title[0])
{
strncpy(metadata->title, tempComment + 6, METADATA_TITLE_MAX - 1);
metadata->title[METADATA_TITLE_MAX - 1] = '\0';
trimWhitespace(metadata->title);
}
else if(strncmp(tempComment, "ARTIST=", 7) == 0 && !metadata->artist[0])
{
strncpy(metadata->artist, tempComment + 7, METADATA_ARTIST_MAX - 1);
metadata->artist[METADATA_ARTIST_MAX - 1] = '\0';
trimWhitespace(metadata->artist);
}
else if(strncmp(tempComment, "ALBUMARTIST=", 12) == 0 && !metadata->artist[0])
{
strncpy(metadata->artist, tempComment + 12, METADATA_ARTIST_MAX - 1);
metadata->artist[METADATA_ARTIST_MAX - 1] = '\0';
trimWhitespace(metadata->artist);
}
else if(strncmp(tempComment, "ALBUM=", 6) == 0 && !metadata->album[0])
{
strncpy(metadata->album, tempComment + 6, METADATA_ALBUM_MAX - 1);
metadata->album[METADATA_ALBUM_MAX - 1] = '\0';
trimWhitespace(metadata->album);
}
}
offset += commentLength;
}
}
}
}
free(commentData);
}
}
else
{
/* Skip other block types */
fseek(fp, blockSize, SEEK_CUR);
}
if(isLast)
break;
}
return 0;
}
/**
* Trim leading and trailing whitespace from string
*/
static void trimWhitespace(char* str)
{
char* start = str;
char* end;
if(!str || *str == 0)
return;
/* Trim leading space */
while(isspace((unsigned char)*start))
start++;
if(*start == 0) /* All spaces? */
{
*str = '\0';
return;
}
/* Trim trailing space */
end = start + strlen(start) - 1;
while(end > start && isspace((unsigned char)*end))
end--;
/* Write new null terminator */
end[1] = '\0';
/* Move trimmed string to beginning if needed */
if(start != str)
{
memmove(str, start, strlen(start) + 1);
}
}
/**
* Parse filename for metadata when tags are not available
* Handles formats like "Artist - Album - Track - Title.ext"
*/
static void parseFilename(const char* file, struct metadata_t* metadata)
{
if(!file || !metadata)
return;
/* Extract just the filename without path */
const char* basename = strrchr(file, '/');
if(!basename)
basename = file;
else
basename++; /* Skip the '/' */
/* Remove file extension */
char filename[256];
strncpy(filename, basename, sizeof(filename) - 1);
filename[sizeof(filename) - 1] = '\0';
char* dot = strrchr(filename, '.');
if(dot) *dot = '\0';
/* Look for pattern: "Artist - Album - Track - Title" */
char* parts[4] = {NULL, NULL, NULL, NULL};
int partCount = 0;
char* token = strtok(filename, " - ");
while(token && partCount < 4)
{
parts[partCount] = token;
partCount++;
token = strtok(NULL, " - ");
}
/* Assign parts based on count */
if(partCount >= 4)
{
/* Artist - Album - Track - Title */
strncpy(metadata->artist, parts[0], METADATA_ARTIST_MAX - 1);
strncpy(metadata->album, parts[1], METADATA_ALBUM_MAX - 1);
strncpy(metadata->title, parts[3], METADATA_TITLE_MAX - 1);
}
else if(partCount == 3)
{
/* Artist - Album - Title */
strncpy(metadata->artist, parts[0], METADATA_ARTIST_MAX - 1);
strncpy(metadata->album, parts[1], METADATA_ALBUM_MAX - 1);
strncpy(metadata->title, parts[2], METADATA_TITLE_MAX - 1);
}
else if(partCount == 2)
{
/* Artist - Title */
strncpy(metadata->artist, parts[0], METADATA_ARTIST_MAX - 1);
strncpy(metadata->title, parts[1], METADATA_TITLE_MAX - 1);
}
else if(partCount == 1)
{
/* Just Title */
strncpy(metadata->title, parts[0], METADATA_TITLE_MAX - 1);
}
/* Ensure null termination */
metadata->artist[METADATA_ARTIST_MAX - 1] = '\0';
metadata->album[METADATA_ALBUM_MAX - 1] = '\0';
metadata->title[METADATA_TITLE_MAX - 1] = '\0';
/* Trim whitespace */
trimWhitespace(metadata->artist);
trimWhitespace(metadata->album);
trimWhitespace(metadata->title);
}