diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7d6684c..ac6c63f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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" + } } ] } diff --git a/include/main.h b/include/main.h index 0371faf..7c869b8 100644 --- a/include/main.h +++ b/include/main.h @@ -13,7 +13,7 @@ #define mice_main_h /* Application version */ -#define MICE_VERSION "dev63" +#define MICE_VERSION "dev68" /* Default folder */ #define DEFAULT_DIR "sdmc:/" diff --git a/include/metadata.h b/include/metadata.h index 85b92d4..3221971 100644 --- a/include/metadata.h +++ b/include/metadata.h @@ -13,8 +13,6 @@ struct metadata_t char title[METADATA_TITLE_MAX]; char artist[METADATA_ARTIST_MAX]; char album[METADATA_ALBUM_MAX]; - - }; /** diff --git a/meta/README.md b/meta/README.md index 61edfd6..2bc967e 100644 --- a/meta/README.md +++ b/meta/README.md @@ -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. diff --git a/meta/audio.wav b/meta/audio.wav index 40c29b5..71cda23 100644 Binary files a/meta/audio.wav and b/meta/audio.wav differ diff --git a/source/gui.c b/source/gui.c index 80bc3d5..00d7123 100644 --- a/source/gui.c +++ b/source/gui.c @@ -83,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) { @@ -101,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) { @@ -185,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) { @@ -215,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) { @@ -269,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) { @@ -319,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) { @@ -331,16 +351,20 @@ 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); } /** - -/** - * Display progress bar on top screen + * Display progress bar on the top screen + * @param position Current playback position in seconds + * @param duration Total duration in seconds */ void guiDisplayProgressBar(float position, float duration) { @@ -372,6 +396,7 @@ void guiDisplayProgressBar(float position, float duration) /** * Display current directory path on bottom screen + * @param path Current directory path */ void guiDisplayCurrentPath(const char* path) { diff --git a/source/main.c b/source/main.c index 74a1000..ffeb15b 100644 --- a/source/main.c +++ b/source/main.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -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. */