Compare commits
8 Commits
dev50
...
54a6dfddad
| Author | SHA1 | Date | |
|---|---|---|---|
|
54a6dfddad
|
|||
|
e3683a12b7
|
|||
| 877e40b073 | |||
|
88026628ea
|
|||
|
87669c74a5
|
|||
|
906734a14c
|
|||
|
92647e2f1d
|
|||
|
36924ddfae
|
6
.github/workflows/3ds.yml
vendored
6
.github/workflows/3ds.yml
vendored
@@ -51,9 +51,9 @@ jobs:
|
||||
- name: Build
|
||||
run: make -j"$(nproc)"
|
||||
|
||||
- name: Upload artefacts
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mice-build
|
||||
path: output
|
||||
path: output/
|
||||
if-no-files-found: warn
|
||||
|
||||
67
.vscode/tasks.json
vendored
67
.vscode/tasks.json
vendored
@@ -1,6 +1,24 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Clean build artifacts",
|
||||
"type": "shell",
|
||||
"command": "rm",
|
||||
"args": [
|
||||
"-rf",
|
||||
"${workspaceFolder}/build/*",
|
||||
"${workspaceFolder}/output/3ds-arm/*"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build 3DS ROM",
|
||||
"type": "shell",
|
||||
@@ -12,10 +30,59 @@
|
||||
"problemMatcher": [
|
||||
"$gcc"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build (full)",
|
||||
"type": "shell",
|
||||
"command": "make",
|
||||
"args": [],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$gcc"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "Serve (dev)",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": [ "run", "dev" ],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/server"
|
||||
},
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"panel": "dedicated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Serve (prod)",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": [ "start" ],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/server"
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
Makefile
2
Makefile
@@ -49,7 +49,7 @@ SOURCE_DIRS := source
|
||||
EXTRA_OUTPUT_FILES :=
|
||||
|
||||
LIBRARY_DIRS := $(DEVKITPRO)/libctru $(DEVKITPRO)/portlibs/armv6k $(DEVKITPRO)/portlibs/3ds
|
||||
LIBRARIES := citro2d citro3d png z sidplay mpg123 vorbisidec opusfile opus ogg ctru m
|
||||
LIBRARIES := citro2d citro3d 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
|
||||
|
||||
@@ -15,6 +15,8 @@ else
|
||||
HOST_ARCH := x86_64
|
||||
else ifeq ($(UNAME_M),$(filter $(UNAME_M),i386 i686))
|
||||
HOST_ARCH := i686
|
||||
else ifeq ($(UNAME_M),$(filter $(UNAME_M),aarch64 arm64))
|
||||
HOST_ARCH := arm64
|
||||
else
|
||||
$(error "Unsupported host architecture.")
|
||||
endif
|
||||
|
||||
@@ -109,10 +109,18 @@ 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
|
||||
* Display progress bar on top screen
|
||||
*
|
||||
* \param metadata Pointer to metadata structure with album art
|
||||
* \param position Current position in seconds
|
||||
* \param duration Total duration in seconds
|
||||
*/
|
||||
void guiDisplayAlbumArt(struct metadata_t* metadata);
|
||||
void guiDisplayProgressBar(float position, float duration);
|
||||
|
||||
/**
|
||||
* Display current directory path on bottom screen
|
||||
*
|
||||
* \param path Current directory path
|
||||
*/
|
||||
void guiDisplayCurrentPath(const char* path);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* mice - 3DS Music Player
|
||||
* Copyright (C) 2016 Mahyar Koshkouei
|
||||
* Copyright (C) 2025 sillyangel
|
||||
*
|
||||
* This program comes with ABSOLUTELY NO WARRANTY and is free software. You are
|
||||
* welcome to redistribute it under certain conditions; for details see the
|
||||
@@ -13,7 +13,7 @@
|
||||
#define mice_main_h
|
||||
|
||||
/* Application version */
|
||||
#define MICE_VERSION "dev50"
|
||||
#define MICE_VERSION "dev68"
|
||||
|
||||
/* Default folder */
|
||||
#define DEFAULT_DIR "sdmc:/"
|
||||
|
||||
@@ -13,13 +13,6 @@ struct metadata_t
|
||||
char title[METADATA_TITLE_MAX];
|
||||
char artist[METADATA_ARTIST_MAX];
|
||||
char album[METADATA_ALBUM_MAX];
|
||||
|
||||
/* Album art */
|
||||
uint8_t* albumArt;
|
||||
size_t albumArtSize;
|
||||
int albumArtWidth;
|
||||
int albumArtHeight;
|
||||
bool hasAlbumArt;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -46,11 +39,4 @@ void clearMetadata(struct metadata_t* metadata);
|
||||
*/
|
||||
void displayMetadata(struct metadata_t* metadata, const char* filename);
|
||||
|
||||
/**
|
||||
* Display album art on top screen if available
|
||||
*
|
||||
* \param metadata Pointer to metadata structure containing album art
|
||||
*/
|
||||
void displayAlbumArt(struct metadata_t* metadata);
|
||||
|
||||
#endif
|
||||
@@ -1,4 +1,4 @@
|
||||
## audio.wav
|
||||
|
||||
Ctrmus uses a modified version of [Rad Adventure by Scott Holmes](http://freemusicarchive.org/music/Scott_Holmes/~/Rad_Adventure) of which is licensed under a [Attribution-NonCommercial License](https://creativecommons.org/licenses/by-nc/4.0/).
|
||||
Permission for use in ctrmus is granted.
|
||||
Mice uses [Internal Monologues by Jon Shuemaker](https://freemusicarchive.org/music/jon-shuemaker/anoka-vol2/internal-monologues/) of which is licensed under a [Attribution-NonCommercial 4.0 International License.](https://creativecommons.org/licenses/by-nc/4.0/).
|
||||
Permission for use in mice is granted.
|
||||
|
||||
BIN
meta/audio.wav
BIN
meta/audio.wav
Binary file not shown.
190
source/gui.c
190
source/gui.c
@@ -4,7 +4,6 @@
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <png.h>
|
||||
#include "gui.h"
|
||||
#include "metadata.h"
|
||||
|
||||
@@ -84,6 +83,12 @@ void guiClearBottomScreen(void)
|
||||
|
||||
/**
|
||||
* Draw a simple text string at specified position
|
||||
* @param screen Screen to draw on (GFX_TOP or GFX_BOTTOM)
|
||||
* @param x X position
|
||||
* @param y Y position
|
||||
* @param text Text string to draw
|
||||
* @param color Text color
|
||||
* @param scale Text scale
|
||||
*/
|
||||
void guiDrawText(gfxScreen_t screen, float x, float y, const char* text, u32 color, float scale)
|
||||
{
|
||||
@@ -102,6 +107,8 @@ void guiDrawText(gfxScreen_t screen, float x, float y, const char* text, u32 col
|
||||
|
||||
/**
|
||||
* Display metadata on the top screen
|
||||
* @param metadata Pointer to metadata structure to display
|
||||
* @param filename Filename to display if no title is available
|
||||
*/
|
||||
void guiDisplayMetadata(struct metadata_t* metadata, const char* filename)
|
||||
{
|
||||
@@ -186,6 +193,9 @@ void guiDisplayMetadata(struct metadata_t* metadata, const char* filename)
|
||||
|
||||
/**
|
||||
* Display log messages on the top screen
|
||||
* @param messages Array of log message strings
|
||||
* @param count Number of messages in the array
|
||||
* @param scroll Index of first visible message for scrolling
|
||||
*/
|
||||
void guiDisplayLog(const char** messages, int count, int scroll)
|
||||
{
|
||||
@@ -216,6 +226,10 @@ void guiDisplayLog(const char** messages, int count, int scroll)
|
||||
|
||||
/**
|
||||
* Display file list on the bottom screen
|
||||
* @param files Array of file/folder names
|
||||
* @param count Number of entries in the array
|
||||
* @param selected Index of currently selected entry
|
||||
* @param scroll Index of first visible entry for scrolling
|
||||
*/
|
||||
void guiDisplayFileList(const char** files, int count, int selected, int scroll)
|
||||
{
|
||||
@@ -225,10 +239,10 @@ void guiDisplayFileList(const char** files, int count, int selected, int scroll)
|
||||
C2D_SceneBegin(bottomTarget);
|
||||
|
||||
C2D_Text text;
|
||||
float y = 10.0f;
|
||||
float y = 18.0f; /* Start below path display */
|
||||
float scale = 0.5f;
|
||||
float lineHeight = 16.0f;
|
||||
int maxLines = 14;
|
||||
int maxLines = 13; /* One less line due to path at top */
|
||||
|
||||
C2D_TextBufClear(textBuf);
|
||||
|
||||
@@ -270,6 +284,10 @@ void guiDisplayFileList(const char** files, int count, int selected, int scroll)
|
||||
|
||||
/**
|
||||
* Display playback controls and status on the top screen
|
||||
* @param isPlaying Whether playback is active
|
||||
* @param isPaused Whether playback is paused
|
||||
* @param position Current playback position in seconds
|
||||
* @param duration Total duration in seconds
|
||||
*/
|
||||
void guiDisplayPlaybackStatus(bool isPlaying, bool isPaused, float position, float duration)
|
||||
{
|
||||
@@ -320,6 +338,7 @@ void guiDisplayPlaybackStatus(bool isPlaying, bool isPaused, float position, flo
|
||||
|
||||
/**
|
||||
* Display version text and credits at bottom of bottom screen
|
||||
* @param version Version string to display
|
||||
*/
|
||||
void guiDisplayVersion(const char* version)
|
||||
{
|
||||
@@ -332,135 +351,68 @@ void guiDisplayVersion(const char* version)
|
||||
C2D_TextBufClear(textBuf);
|
||||
|
||||
/* Display "mice - by sillyangel" at bottom center */
|
||||
const char* credits = "mice - by sillyangel";
|
||||
char credits[64];
|
||||
if(version && version[0])
|
||||
snprintf(credits, sizeof(credits), "mice %s - by sillyangel", version);
|
||||
else
|
||||
snprintf(credits, sizeof(credits), "mice - by sillyangel");
|
||||
C2D_TextParse(&text, textBuf, credits);
|
||||
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
|
||||
* Display progress bar on the top screen
|
||||
* @param position Current playback position in seconds
|
||||
* @param duration Total duration in seconds
|
||||
*/
|
||||
static void pngReadCallback(png_structp png_ptr, png_bytep data, png_size_t length)
|
||||
void guiDisplayProgressBar(float position, float duration)
|
||||
{
|
||||
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)
|
||||
if(duration <= 0)
|
||||
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;
|
||||
/* Progress bar at bottom of top screen */
|
||||
float barY = 205.0f;
|
||||
float barX = 10.0f;
|
||||
float barWidth = 380.0f;
|
||||
float barHeight = 6.0f;
|
||||
|
||||
png_infop info_ptr = png_create_info_struct(png_ptr);
|
||||
if(!info_ptr)
|
||||
/* Background bar */
|
||||
C2D_DrawRectSolid(barX, barY, 0.5f, barWidth, barHeight, C2D_Color32(50, 50, 60, 255));
|
||||
|
||||
/* Progress fill */
|
||||
float progress = position / duration;
|
||||
if(progress > 1.0f) progress = 1.0f;
|
||||
if(progress < 0.0f) progress = 0.0f;
|
||||
|
||||
float fillWidth = barWidth * progress;
|
||||
if(fillWidth > 0)
|
||||
{
|
||||
png_destroy_read_struct(&png_ptr, NULL, NULL);
|
||||
return;
|
||||
C2D_DrawRectSolid(barX, barY, 0.5f, fillWidth, barHeight, GUI_COLOR_ACCENT);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display current directory path on bottom screen
|
||||
* @param path Current directory path
|
||||
*/
|
||||
void guiDisplayCurrentPath(const char* path)
|
||||
{
|
||||
if(!path || !textBuf)
|
||||
return;
|
||||
|
||||
C2D_SceneBegin(bottomTarget);
|
||||
|
||||
C2D_Text text;
|
||||
C2D_TextBufClear(textBuf);
|
||||
|
||||
/* Display path at top of bottom screen */
|
||||
char pathBuf[64];
|
||||
snprintf(pathBuf, sizeof(pathBuf), "%.55s", path);
|
||||
|
||||
C2D_TextParse(&text, textBuf, pathBuf);
|
||||
C2D_TextOptimize(&text);
|
||||
C2D_DrawText(&text, C2D_WithColor, 5.0f, 2.0f, 0.5f, 0.35f, 0.35f, GUI_COLOR_TEXT_DIM);
|
||||
}
|
||||
|
||||
145
source/main.c
145
source/main.c
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* mice - 3DS Music Player
|
||||
* Copyright (C) 2016 Mahyar Koshkouei
|
||||
* Copyright (C) 2025 sillyangel
|
||||
*
|
||||
* This program comes with ABSOLUTELY NO WARRANTY and is free software. You are
|
||||
* welcome to redistribute it under certain conditions; for details see the
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <dirent.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
@@ -146,6 +147,125 @@ static int cmpstringp(const void *p1, const void *p2)
|
||||
return strcasecmp(* (char * const *) p1, * (char * const *) p2);
|
||||
}
|
||||
|
||||
/* Check filename extension against supported audio types. Case-insensitive. */
|
||||
static bool isMusicFilename(const char *name)
|
||||
{
|
||||
if(name == NULL)
|
||||
return false;
|
||||
|
||||
const char *ext = strrchr(name, '.');
|
||||
if(ext == NULL || *(ext + 1) == '\0')
|
||||
return false;
|
||||
|
||||
ext++; /* skip dot */
|
||||
char lext[16];
|
||||
size_t i = 0;
|
||||
for(; i < sizeof(lext)-1 && ext[i]; i++)
|
||||
lext[i] = tolower((unsigned char)ext[i]);
|
||||
lext[i] = '\0';
|
||||
|
||||
const char *allowed[] = {"mp3","wav","flac","ogg","opus","sid","m4a","aac", NULL};
|
||||
for(int j = 0; allowed[j] != NULL; j++)
|
||||
if(strcmp(lext, allowed[j]) == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Build a list of music filenames (basenames) in the given directory. Caller must free array and each string. */
|
||||
static int buildMusicFileListInDir(const char *dirpath, char ***outFiles)
|
||||
{
|
||||
DIR *dp;
|
||||
struct dirent *ep;
|
||||
char **files = NULL;
|
||||
int fileNum = 0;
|
||||
|
||||
if((dp = opendir(dirpath)) == NULL)
|
||||
return -1;
|
||||
|
||||
while((ep = readdir(dp)) != NULL)
|
||||
{
|
||||
if(ep->d_type == DT_DIR)
|
||||
continue;
|
||||
|
||||
if(!isMusicFilename(ep->d_name))
|
||||
continue;
|
||||
|
||||
files = realloc(files, (fileNum + 1) * sizeof(char*));
|
||||
files[fileNum] = strdup(ep->d_name);
|
||||
fileNum++;
|
||||
}
|
||||
|
||||
if(fileNum > 0)
|
||||
qsort(files, fileNum, sizeof(char*), cmpstringp);
|
||||
|
||||
closedir(dp);
|
||||
*outFiles = files;
|
||||
return fileNum;
|
||||
}
|
||||
|
||||
/* Play next file in the same folder as the currently-playing file. Returns 0 on success, -1 on failure or no-next. */
|
||||
static int playNextFromPath(struct playbackInfo_t *playbackInfo)
|
||||
{
|
||||
if(playbackInfo == NULL || playbackInfo->file[0] == '\0')
|
||||
return -1;
|
||||
|
||||
char fullcpy[PATH_MAX];
|
||||
strncpy(fullcpy, playbackInfo->file, sizeof(fullcpy));
|
||||
fullcpy[sizeof(fullcpy)-1] = '\0';
|
||||
|
||||
/* find last slash */
|
||||
char *slash = strrchr(fullcpy, '/');
|
||||
char dirpath[PATH_MAX];
|
||||
char *basename = NULL;
|
||||
|
||||
if(slash == NULL)
|
||||
{
|
||||
/* No directory component; assume current dir */
|
||||
if(getcwd(dirpath, sizeof(dirpath)) == NULL)
|
||||
return -1;
|
||||
basename = fullcpy;
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t dirlen = slash - fullcpy;
|
||||
if(dirlen >= sizeof(dirpath))
|
||||
return -1;
|
||||
memcpy(dirpath, fullcpy, dirlen);
|
||||
dirpath[dirlen] = '\0';
|
||||
basename = slash + 1;
|
||||
}
|
||||
|
||||
char **files = NULL;
|
||||
int count = buildMusicFileListInDir(dirpath, &files);
|
||||
if(count <= 0)
|
||||
return -1;
|
||||
|
||||
int index = -1;
|
||||
for(int i = 0; i < count; i++)
|
||||
{
|
||||
if(strcmp(files[i], basename) == 0)
|
||||
{
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int result = -1;
|
||||
if(index >= 0 && index + 1 < count)
|
||||
{
|
||||
char nextpath[PATH_MAX];
|
||||
snprintf(nextpath, sizeof(nextpath), "%s/%s", dirpath, files[index+1]);
|
||||
result = changeFile(nextpath, playbackInfo);
|
||||
}
|
||||
|
||||
for(int i = 0; i < count; i++)
|
||||
free(files[i]);
|
||||
free(files);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the list of files and folders in current directory to an array.
|
||||
*/
|
||||
@@ -614,18 +734,24 @@ int main(int argc __attribute__((unused)), char **argv __attribute__((unused)))
|
||||
/* Display metadata if we have any */
|
||||
if(currentMetadata.title[0] || currentMetadata.artist[0] || currentMetadata.album[0])
|
||||
{
|
||||
const char* currentFile = (fileNum > 0 && fileNum <= dirList.dirNum + dirList.fileNum) ?
|
||||
(fileNum > dirList.dirNum ? dirList.files[fileNum - dirList.dirNum - 1] : "..") : "";
|
||||
guiDisplayMetadata(¤tMetadata, currentFile);
|
||||
guiDisplayAlbumArt(¤tMetadata);
|
||||
const char* currentFile = (fileNum > 0 && fileNum <= dirList.dirNum + dirList.fileNum) ?
|
||||
(fileNum > dirList.dirNum ? dirList.files[fileNum - dirList.dirNum - 1] : "..") : "";
|
||||
guiDisplayMetadata(¤tMetadata, currentFile);
|
||||
}
|
||||
|
||||
/* Calculate scroll position to keep selection visible (14 lines visible) */
|
||||
/* Calculate scroll position to keep selection visible (13 lines visible with path) */
|
||||
int scroll = from;
|
||||
if(fileNum < scroll)
|
||||
scroll = fileNum;
|
||||
else if(fileNum >= scroll + 14)
|
||||
scroll = fileNum - 13;
|
||||
else if(fileNum >= scroll + 13)
|
||||
scroll = fileNum - 12;
|
||||
|
||||
/* Display current directory path */
|
||||
char currentPath[256];
|
||||
if(getcwd(currentPath, sizeof(currentPath)))
|
||||
{
|
||||
guiDisplayCurrentPath(currentPath);
|
||||
}
|
||||
|
||||
/* Display file list on bottom screen */
|
||||
guiDisplayFileList(fileList, fileListCount, fileNum, scroll);
|
||||
@@ -633,11 +759,12 @@ int main(int argc __attribute__((unused)), char **argv __attribute__((unused)))
|
||||
/* Display logs on top screen */
|
||||
guiDisplayLog((const char**)logMessages, logMessageCount, logScroll);
|
||||
|
||||
/* Display playback status */
|
||||
/* Display playback status and progress bar */
|
||||
if(playbackInfo.samples_per_second > 0)
|
||||
{
|
||||
float position = (float)playbackInfo.samples_played / playbackInfo.samples_per_second;
|
||||
float duration = (float)playbackInfo.samples_total / playbackInfo.samples_per_second;
|
||||
guiDisplayProgressBar(position, duration);
|
||||
guiDisplayPlaybackStatus(isPlaying(), isPaused(), position, duration);
|
||||
}
|
||||
|
||||
|
||||
@@ -83,16 +83,7 @@ void clearMetadata(struct metadata_t* metadata)
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,19 +95,6 @@ void displayMetadata(struct metadata_t* metadata, const char* filename)
|
||||
guiDisplayMetadata(metadata, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -278,51 +256,8 @@ static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata)
|
||||
}
|
||||
else if(strncmp(frameId, "APIC", 4) == 0) /* Attached Picture */
|
||||
{
|
||||
/* Extract album art data */
|
||||
if(frameSize > 10 && !metadata->hasAlbumArt)
|
||||
{
|
||||
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);
|
||||
}
|
||||
/* Skip album art data */
|
||||
fseek(fp, frameSize, SEEK_CUR);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -496,68 +431,8 @@ static int extractFlacMetadata(FILE* fp, struct metadata_t* metadata)
|
||||
|
||||
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);
|
||||
}
|
||||
/* Skip picture block */
|
||||
fseek(fp, blockSize, SEEK_CUR);
|
||||
}
|
||||
else if(blockType == 4) /* VORBIS_COMMENT */
|
||||
{
|
||||
|
||||
44
unistore/mice.unistore
Normal file
44
unistore/mice.unistore
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"storeInfo": {
|
||||
"title": "mice unistore",
|
||||
"author": "sillyangel",
|
||||
"description": "A music player for the Nintendo 3DS",
|
||||
"url": "https://git.sillyangel.dev/angel/mice-3ds/raw/branch/master/unistore/mice.unistore",
|
||||
"file": "mice.unistore",
|
||||
"sheetURL": "https://git.sillyangel.dev/angel/mice-3ds/raw/branch/master/unistore/spritesheet/sheet.t3x",
|
||||
"sheet": "sheet.t3x",
|
||||
"version": 4,
|
||||
"revision": 0
|
||||
},
|
||||
"storeContent": [
|
||||
{
|
||||
"info": {
|
||||
"title": "mice",
|
||||
"author": "sillyangel",
|
||||
"description": "A music player for the Nintendo 3DS. Plays PCM WAV, AIFF, FLAC, Opus, Vorbis and MP3 files.",
|
||||
"category": ["utility", "music"],
|
||||
"console": ["3DS"],
|
||||
"icon_index": 0,
|
||||
"sheet_index": 0,
|
||||
"last_updated": "2025-12-07 at 00:00 (UTC)",
|
||||
"license": "gpl-3.0",
|
||||
"version": "dev63"
|
||||
}
|
||||
"mice.cia": [
|
||||
{
|
||||
"type": "downloadFile",
|
||||
"file": "hhttps://git.sillyangel.dev/angel/mice-3ds/releases/download/latest/mice.cia",
|
||||
"output": "sdmc:/mice.cia"
|
||||
},
|
||||
{
|
||||
"type": "installCia",
|
||||
"file": "/mice.cia"
|
||||
},
|
||||
{
|
||||
"type": "deleteFile",
|
||||
"file": "sdmc:/mice.cia"
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
unistore/spritesheet/0.png
Normal file
BIN
unistore/spritesheet/0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
3
unistore/spritesheet/sheet.t3s
Normal file
3
unistore/spritesheet/sheet.t3s
Normal file
@@ -0,0 +1,3 @@
|
||||
--atlas -f rgba -z auto
|
||||
|
||||
0.png
|
||||
BIN
unistore/spritesheet/sheet.t3x
Normal file
BIN
unistore/spritesheet/sheet.t3x
Normal file
Binary file not shown.
Reference in New Issue
Block a user