#include <3ds.h> #include #include #include #include #include "metadata.h" #include "file.h" #include "all.h" #include "gui.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); } /** * Display metadata on the top screen * (Now uses GUI rendering - this is a wrapper for compatibility) */ void displayMetadata(struct metadata_t* metadata, const char* filename) { guiDisplayMetadata(metadata, filename); } /** * 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; /* Text encoding byte: * 0x00 = ISO-8859-1 * 0x01 = UTF-16 with BOM * 0x02 = UTF-16BE without BOM * 0x03 = UTF-8 */ uint8_t encoding = frameData[0]; char* text = frameData + 1; int textLen = frameSize - 1; /* Skip UTF-16 BOM if present */ if(encoding == 0x01 && textLen >= 2) { if((uint8_t)text[0] == 0xFF && (uint8_t)text[1] == 0xFE) { text += 2; textLen -= 2; } else if((uint8_t)text[0] == 0xFE && (uint8_t)text[1] == 0xFF) { text += 2; textLen -= 2; } } /* 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) { /* Handle different encodings */ if(encoding == 0x01 || encoding == 0x02) /* UTF-16 */ { /* Convert UTF-16 to ASCII (simplified - just take every other byte) */ int outPos = 0; for(int i = 0; i < textLen && outPos < (int)maxLen; i += 2) { if(text[i] >= 0x20 && text[i] < 0x7F) dest[outPos++] = text[i]; else if(text[i] == 0 && text[i+1] == 0) break; } dest[outPos] = 0; } else /* ISO-8859-1 or UTF-8 */ { /* Copy as-is, filtering out non-ASCII characters */ int outPos = 0; for(int i = 0; i < textLen && outPos < (int)maxLen; i++) { if((uint8_t)text[i] >= 0x20 && (uint8_t)text[i] < 0x7F) dest[outPos++] = text[i]; else if(text[i] == 0) break; } dest[outPos] = 0; } trimWhitespace(dest); } } free(frameData); } else if(strncmp(frameId, "APIC", 4) == 0) /* Attached Picture */ { /* Skip album art data */ 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 == 6) /* PICTURE */ { /* Skip picture block */ fseek(fp, blockSize, SEEK_CUR); } else 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); }