diff --git a/Makefile b/Makefile index 1668993..f9c0dbc 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ SOURCE_DIRS := source EXTRA_OUTPUT_FILES := LIBRARY_DIRS := $(DEVKITPRO)/libctru $(DEVKITPRO)/portlibs/armv6k $(DEVKITPRO)/portlibs/3ds -LIBRARIES := citro2d citro3d sidplay mpg123 vorbisidec opusfile opus ogg ctru m +LIBRARIES := citro2d citro3d png z sidplay mpg123 vorbisidec opusfile opus ogg ctru m BUILD_FLAGS := -Wall -Wextra -I$(DEVKITPRO)/libctru/include -I$(DEVKITPRO)/portlibs/armv6k/include/opus -I$(DEVKITPRO)/portlibs/3ds/include/opus -O3 -g3 -ffunction-sections -fdata-sections # -O0 -g3 -fstack-protector-strong -fsanitize=undefined -fsanitize-trap diff --git a/include/gui.h b/include/gui.h index 099e519..7f7d28b 100644 --- a/include/gui.h +++ b/include/gui.h @@ -108,4 +108,11 @@ void guiDisplayVersion(const char* version); */ void guiDrawText(gfxScreen_t screen, float x, float y, const char* text, u32 color, float scale); +/** + * Display album art on top screen + * + * \param metadata Pointer to metadata structure with album art + */ +void guiDisplayAlbumArt(struct metadata_t* metadata); + #endif diff --git a/include/main.h b/include/main.h index 946d6fd..d3cc0f0 100644 --- a/include/main.h +++ b/include/main.h @@ -13,7 +13,7 @@ #define mice_main_h /* Application version */ -#define MICE_VERSION "dev43" +#define MICE_VERSION "dev50" /* Default folder */ #define DEFAULT_DIR "sdmc:/" diff --git a/source/gui.c b/source/gui.c index f92bc57..aab016f 100644 --- a/source/gui.c +++ b/source/gui.c @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include "gui.h" #include "metadata.h" @@ -180,14 +182,6 @@ void guiDisplayMetadata(struct metadata_t* metadata, const char* filename) C2D_TextOptimize(&text); C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale * 0.8f, scale * 0.8f, GUI_COLOR_TEXT_DIM); } - - /* Draw album art indicator if available */ - if(metadata->hasAlbumArt) - { - C2D_TextParse(&text, textBuf, "[Art]"); - C2D_TextOptimize(&text); - C2D_DrawText(&text, C2D_WithColor, 350.0f, 10.0f, 0.5f, 0.4f, 0.4f, GUI_COLOR_ACCENT); - } } /** @@ -343,3 +337,130 @@ void guiDisplayVersion(const char* version) C2D_TextOptimize(&text); C2D_DrawText(&text, C2D_WithColor, 80.0f, 220.0f, 0.5f, 0.45f, 0.45f, GUI_COLOR_TEXT_DIM); } + +/** + * PNG read callback for memory buffer + */ +static void pngReadCallback(png_structp png_ptr, png_bytep data, png_size_t length) +{ + uint8_t** buffer_ptr = (uint8_t**)png_get_io_ptr(png_ptr); + memcpy(data, *buffer_ptr, length); + *buffer_ptr += length; +} + +/** + * Display album art on top screen + */ +void guiDisplayAlbumArt(struct metadata_t* metadata) +{ + if(!metadata || !metadata->hasAlbumArt || !metadata->albumArt) + return; + + C2D_SceneBegin(topTarget); + + /* Decode PNG image */ + png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if(!png_ptr) + return; + + png_infop info_ptr = png_create_info_struct(png_ptr); + if(!info_ptr) + { + png_destroy_read_struct(&png_ptr, NULL, NULL); + return; + } + + if(setjmp(png_jmpbuf(png_ptr))) + { + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + return; + } + + /* Set up custom read function */ + uint8_t* buffer_ptr = metadata->albumArt; + png_set_read_fn(png_ptr, &buffer_ptr, pngReadCallback); + + /* Read PNG info */ + png_read_info(png_ptr, info_ptr); + + int width = png_get_image_width(png_ptr, info_ptr); + int height = png_get_image_height(png_ptr, info_ptr); + png_byte color_type = png_get_color_type(png_ptr, info_ptr); + png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr); + + /* Convert to RGBA8 */ + if(bit_depth == 16) + png_set_strip_16(png_ptr); + if(color_type == PNG_COLOR_TYPE_PALETTE) + png_set_palette_to_rgb(png_ptr); + if(color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) + png_set_expand_gray_1_2_4_to_8(png_ptr); + if(png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) + png_set_tRNS_to_alpha(png_ptr); + if(color_type == PNG_COLOR_TYPE_RGB || + color_type == PNG_COLOR_TYPE_GRAY || + color_type == PNG_COLOR_TYPE_PALETTE) + png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER); + if(color_type == PNG_COLOR_TYPE_GRAY || + color_type == PNG_COLOR_TYPE_GRAY_ALPHA) + png_set_gray_to_rgb(png_ptr); + + png_read_update_info(png_ptr, info_ptr); + + /* Allocate image buffer */ + png_bytep* row_pointers = (png_bytep*)malloc(sizeof(png_bytep) * height); + if(!row_pointers) + { + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + return; + } + + for(int y = 0; y < height; y++) + { + row_pointers[y] = (png_byte*)malloc(png_get_rowbytes(png_ptr, info_ptr)); + if(!row_pointers[y]) + { + for(int i = 0; i < y; i++) + free(row_pointers[i]); + free(row_pointers); + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + return; + } + } + + /* Read image data */ + png_read_image(png_ptr, row_pointers); + + /* Scale to fit in top-right corner (max 60x60) */ + int displayWidth = width; + int displayHeight = height; + if(width > 60 || height > 60) + { + float scale = 60.0f / (width > height ? width : height); + displayWidth = (int)(width * scale); + displayHeight = (int)(height * scale); + } + + /* Draw the image pixel by pixel */ + float startX = 330.0f; /* Top right corner */ + float startY = 10.0f; + + for(int y = 0; y < displayHeight; y++) + { + int srcY = (y * height) / displayHeight; + for(int x = 0; x < displayWidth; x++) + { + int srcX = (x * width) / displayWidth; + png_bytep px = &(row_pointers[srcY][srcX * 4]); + u32 color = C2D_Color32(px[0], px[1], px[2], px[3]); + C2D_DrawRectSolid(startX + x, startY + y, 0.5f, 1, 1, color); + } + } + + /* Clean up */ + for(int y = 0; y < height; y++) + free(row_pointers[y]); + free(row_pointers); + + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); +} diff --git a/source/main.c b/source/main.c index 0a0eb8d..3199e8a 100644 --- a/source/main.c +++ b/source/main.c @@ -617,6 +617,7 @@ int main(int argc __attribute__((unused)), char **argv __attribute__((unused))) const char* currentFile = (fileNum > 0 && fileNum <= dirList.dirNum + dirList.fileNum) ? (fileNum > dirList.dirNum ? dirList.files[fileNum - dirList.dirNum - 1] : "..") : ""; guiDisplayMetadata(¤tMetadata, currentFile); + guiDisplayAlbumArt(¤tMetadata); } /* Calculate scroll position to keep selection visible (14 lines visible) */ diff --git a/source/metadata.c b/source/metadata.c index aca54e9..7f5033b 100644 --- a/source/metadata.c +++ b/source/metadata.c @@ -194,56 +194,135 @@ static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata) continue; } - if(fread(frameData, 1, frameSize, fp) == frameSize) + 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) { - 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) + if((uint8_t)text[0] == 0xFF && (uint8_t)text[1] == 0xFE) { - dest = metadata->title; - maxLen = METADATA_TITLE_MAX - 1; + text += 2; + textLen -= 2; } - else if(strncmp(frameId, "TPE1", 4) == 0) + else if((uint8_t)text[0] == 0xFE && (uint8_t)text[1] == 0xFF) { - 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); + text += 2; + textLen -= 2; } } - free(frameData); + /* 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 */ { - /* Basic album art detection - just store size info for now */ + /* Extract album art data */ 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; + uint8_t* frameData = malloc(frameSize); + if(frameData && fread(frameData, 1, frameSize, fp) == frameSize) + { + /* ID3v2 APIC frame format: + * - Text encoding (1 byte) + * - MIME type (null-terminated string) + * - Picture type (1 byte) + * - Description (null-terminated string) + * - Picture data + */ + uint8_t* ptr = frameData; + ptr++; /* Skip text encoding */ + + /* Skip MIME type */ + while(*ptr != 0 && (ptr - frameData) < (int)frameSize) ptr++; + ptr++; /* Skip null terminator */ + + ptr++; /* Skip picture type */ + + /* Skip description */ + while(*ptr != 0 && (ptr - frameData) < (int)frameSize) ptr++; + ptr++; /* Skip null terminator */ + + /* Remaining data is the image */ + size_t imageSize = frameSize - (ptr - frameData); + if(imageSize > 0) + { + metadata->albumArt = malloc(imageSize); + if(metadata->albumArt) + { + memcpy(metadata->albumArt, ptr, imageSize); + metadata->albumArtSize = imageSize; + metadata->hasAlbumArt = true; + } + } + } + if(frameData) free(frameData); + } + else + { + fseek(fp, frameSize, SEEK_CUR); } - fseek(fp, frameSize, SEEK_CUR); } else { @@ -415,7 +494,72 @@ static int extractFlacMetadata(FILE* fp, struct metadata_t* metadata) blockType = blockHeader[0] & 0x7F; blockSize = (blockHeader[1] << 16) | (blockHeader[2] << 8) | blockHeader[3]; - if(blockType == 4) /* VORBIS_COMMENT */ + if(blockType == 6) /* PICTURE */ + { + /* Extract album art from FLAC picture block */ + if(!metadata->hasAlbumArt && blockSize > 32) + { + uint8_t* pictureData = malloc(blockSize); + if(pictureData && fread(pictureData, 1, blockSize, fp) == blockSize) + { + /* FLAC picture block format: + * - Picture type (4 bytes BE) + * - MIME type length (4 bytes BE) + * - MIME type string + * - Description length (4 bytes BE) + * - Description string + * - Width (4 bytes BE) + * - Height (4 bytes BE) + * - Depth (4 bytes BE) + * - Colors (4 bytes BE) + * - Picture data length (4 bytes BE) + * - Picture data + */ + uint32_t offset = 4; /* Skip picture type */ + + /* Skip MIME type */ + uint32_t mimeLen = (pictureData[offset] << 24) | (pictureData[offset+1] << 16) | + (pictureData[offset+2] << 8) | pictureData[offset+3]; + offset += 4 + mimeLen; + + /* Skip description */ + if(offset + 4 <= blockSize) + { + uint32_t descLen = (pictureData[offset] << 24) | (pictureData[offset+1] << 16) | + (pictureData[offset+2] << 8) | pictureData[offset+3]; + offset += 4 + descLen; + } + + /* Skip width, height, depth, colors (16 bytes) */ + offset += 16; + + /* Get picture data length */ + if(offset + 4 <= blockSize) + { + uint32_t picLen = (pictureData[offset] << 24) | (pictureData[offset+1] << 16) | + (pictureData[offset+2] << 8) | pictureData[offset+3]; + offset += 4; + + if(offset + picLen <= blockSize && picLen > 0) + { + metadata->albumArt = malloc(picLen); + if(metadata->albumArt) + { + memcpy(metadata->albumArt, pictureData + offset, picLen); + metadata->albumArtSize = picLen; + metadata->hasAlbumArt = true; + } + } + } + } + if(pictureData) free(pictureData); + } + else + { + fseek(fp, blockSize, SEEK_CUR); + } + } + else if(blockType == 4) /* VORBIS_COMMENT */ { /* FLAC uses Vorbis comments for metadata */ /* This is a simplified implementation */