/** * mice - 3DS Music Player * Copyright (C) 2016 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 * LICENSE file. */ #include <3ds.h> #include #include #include #include #include #include #include "all.h" #include "error.h" #include "file.h" #include "main.h" #include "metadata.h" #include "playback.h" #include "gui.h" volatile bool runThreads = true; /* Log message buffer for GUI display */ #define MAX_LOG_MESSAGES 100 static char* logMessages[MAX_LOG_MESSAGES] = {0}; static int logMessageCount = 0; static int logScroll = 0; static void addLogMessage(const char* msg) { if (logMessageCount >= MAX_LOG_MESSAGES) { /* Remove oldest message */ free(logMessages[0]); memmove(logMessages, logMessages + 1, (MAX_LOG_MESSAGES - 1) * sizeof(char*)); logMessageCount--; } logMessages[logMessageCount++] = strdup(msg); } /** * Prints the current key mappings (removed - not needed for GUI) */ /* Controls are now implied by the GUI interface */ /** * Allows the playback thread to return any error messages that it may * encounter. * * \param infoIn Struct containing addresses of the event, the error code, * and an optional error string. */ void playbackWatchdog(void* infoIn) { struct watchdogInfo* info = infoIn; while(runThreads) { svcWaitSynchronization(*info->errInfo->failEvent, U64_MAX); svcClearEvent(*info->errInfo->failEvent); if(*info->errInfo->error > 0) { char errorMsg[256]; snprintf(errorMsg, sizeof(errorMsg), "Error %d: %s", *info->errInfo->error, mice_strerror(*info->errInfo->error)); addLogMessage(errorMsg); } else if (*info->errInfo->error == -1) { /* Used to signify that playback has stopped. * Not technically an error. Don't spam logs. */ /* addLogMessage("Stopped"); */ } } return; } /** * Stop the currently playing file (if there is one) and play another file. * * \param ep_file File to play. * \param playbackInfo Information that the playback thread requires to * play file. */ static int changeFile(const char* ep_file, struct playbackInfo_t* playbackInfo) { s32 prio; static Thread thread = NULL; if(ep_file != NULL && getFileType(ep_file) == FILE_TYPE_ERROR) { *playbackInfo->errInfo->error = errno; svcSignalEvent(*playbackInfo->errInfo->failEvent); return -1; } /** * If music is playing, stop it. Only one playback thread should be playing * at any time. */ if(thread != NULL) { /* Tell the thread to stop playback before we join it */ stopPlayback(); threadJoin(thread, U64_MAX); threadFree(thread); thread = NULL; } /* If file is NULL, then only thread termination was requested. */ if(ep_file == NULL || playbackInfo == NULL) return 0; //playbackInfo->file = strdup(ep_file); if (memccpy(playbackInfo->file, ep_file, '\0', sizeof(playbackInfo->file)) == NULL) { addLogMessage("Error: File path too long"); return -1; } /* Remove "Playing:" message that was causing display issues */ /* printf("Playing: %s\n", playbackInfo->file); */ playbackInfo->samples_total = 0; playbackInfo->samples_played = 0; playbackInfo->samples_per_second = 0; svcGetThreadPriority(&prio, CUR_THREAD_HANDLE); thread = threadCreate(playFile, playbackInfo, 32 * 1024, prio - 1, -2, false); return 0; } static int cmpstringp(const void *p1, const void *p2) { /* The actual arguments to this function are "pointers to pointers to char", but strcmp(3) arguments are "pointers to char", hence the following cast plus dereference */ return strcasecmp(* (char * const *) p1, * (char * const *) p2); } /** * Store the list of files and folders in current directory to an array. */ static int getDir(struct dirList_t* dirList) { DIR *dp; struct dirent *ep; int fileNum = 0; int dirNum = 0; char* wd = getcwd(NULL, 0); if(wd == NULL) goto out; /* Clear strings */ for(int i = 0; i < dirList->dirNum; i++) free(dirList->directories[i]); for(int i = 0; i < dirList->fileNum; i++) free(dirList->files[i]); free(dirList->currentDir); if((dirList->currentDir = strdup(wd)) == NULL) addLogMessage("Memory allocation failure"); if((dp = opendir(wd)) == NULL) goto out; while((ep = readdir(dp)) != NULL) { if(ep->d_type == DT_DIR) { /* Add more space for another pointer to a dirent struct */ dirList->directories = realloc(dirList->directories, (dirNum + 1) * sizeof(char*)); if((dirList->directories[dirNum] = strdup(ep->d_name)) == NULL) addLogMessage("Memory allocation failure"); dirNum++; continue; } /* Add more space for another pointer to a dirent struct */ dirList->files = realloc(dirList->files, (fileNum + 1) * sizeof(char*)); if((dirList->files[fileNum] = strdup(ep->d_name)) == NULL) addLogMessage("Memory allocation failure"); fileNum++; } qsort(&dirList->files[0], fileNum, sizeof(char *), cmpstringp); qsort(&dirList->directories[0], dirNum, sizeof(char *), cmpstringp); dirList->dirNum = dirNum; dirList->fileNum = fileNum; if(closedir(dp) != 0) goto out; out: free(wd); return fileNum + dirNum; } /** * Build file list for GUI display. * Creates a combined list of directories and files for rendering. */ static void buildFileListForGUI(struct dirList_t dirList, const char*** outList, int* outCount, int from) { static const char* combinedList[512]; static char entryBuffer[512][256]; int index = 0; /* Add parent directory option */ if(from == 0) { snprintf(entryBuffer[index], sizeof(entryBuffer[index]), "../"); combinedList[index] = entryBuffer[index]; index++; } /* Add all directories */ for(int i = 0; i < dirList.dirNum && index < 512; i++) { snprintf(entryBuffer[index], sizeof(entryBuffer[index]), "%s/", dirList.directories[i]); combinedList[index] = entryBuffer[index]; index++; } /* Add all files */ for(int i = 0; i < dirList.fileNum && index < 512; i++) { snprintf(entryBuffer[index], sizeof(entryBuffer[index]), "%s", dirList.files[i]); combinedList[index] = entryBuffer[index]; index++; } *outList = combinedList; *outCount = index; } /** * Dummy function kept for compatibility (no longer used with GUI) */ static int listDir(int from __attribute__((unused)), int max __attribute__((unused)), int select __attribute__((unused)), struct dirList_t dirList __attribute__((unused))) { /* This function is no longer used with GUI rendering */ return 0; } /** * Get number of files in current working folder * * \return Number of files in current working folder, -1 on failure with * errno set. */ int getNumberFiles(void) { DIR *dp; struct dirent *ep; int ret = 0; if((dp = opendir(".")) == NULL) goto err; while((ep = readdir(dp)) != NULL) ret++; closedir(dp); out: return ret; err: ret = -1; goto out; } int main(int argc __attribute__((unused)), char **argv __attribute__((unused))) { int fileMax; int fileNum = 0; int from = 0; Thread watchdogThread __attribute__((unused)); Handle playbackFailEvent; struct watchdogInfo watchdogInfoIn; struct errInfo_t errInfo; struct playbackInfo_t playbackInfo = { 0 }; volatile int error = 0; struct dirList_t dirList = { 0 }; struct metadata_t currentMetadata = { 0 }; /* ignore key release of L/R if L+R or L+down was pressed */ bool keyLComboPressed = false; bool keyRComboPressed = false; /* Initialize GUI system */ if(guiInit() != 0) { return -1; } svcCreateEvent(&playbackFailEvent, RESET_ONESHOT); errInfo.error = &error; errInfo.failEvent = &playbackFailEvent; watchdogInfoIn.screen = NULL; /* No longer using console */ watchdogInfoIn.errInfo = &errInfo; watchdogThread = threadCreate(playbackWatchdog, &watchdogInfoIn, 4 * 1024, 0x20, -2, true); playbackInfo.errInfo = &errInfo; /* position of parent folder in parent directory */ int prevPosition[MAX_DIRECTORIES] = {0}; int prevFrom[MAX_DIRECTORIES] = {0}; int oldFileNum, oldFrom; chdir(DEFAULT_DIR); chdir("MUSIC"); /* TODO: Not actually possible to get less than 0 */ if(getDir(&dirList) < 0) { addLogMessage("Unable to obtain directory information"); goto err; } fileMax = getNumberFiles(); /** * This allows for music to continue playing through the headphones whilst * the 3DS is closed. */ aptSetSleepAllowed(false); while(aptMainLoop()) { u32 kDown; u32 kHeld; u32 kUp; static u64 mill = 0; /* Begin GUI frame */ guiBeginFrame(); guiClearTopScreen(); guiClearBottomScreen(); hidScanInput(); kDown = hidKeysDown(); kHeld = hidKeysHeld(); kUp = hidKeysUp(); /* Exit mice */ if(kDown & KEY_START) break; #ifdef DEBUG /* Debug info logged if needed */ #endif if(kDown) mill = osGetTime(); if(kHeld & KEY_L) { /* Pause/Play */ if(kDown & (KEY_R | KEY_UP)) { if(isPlaying() == false) continue; togglePlayback(); keyLComboPressed = true; // distinguish between L+R and L+Up if (KEY_R & kDown) { keyRComboPressed = true; } continue; } /* Show controls - no longer needed with GUI */ if(kDown & KEY_LEFT) { keyLComboPressed = true; continue; } } // if R is pressed first if ((kHeld & KEY_R) && (kDown & KEY_L)) { if(isPlaying() == false) continue; togglePlayback(); keyLComboPressed = true; keyRComboPressed = true; continue; } if((kDown & KEY_UP || ((kHeld & KEY_UP) && (osGetTime() - mill > 500))) && fileNum > 0) { fileNum--; // one line taken up by cwd, other by ../ if(fileMax - fileNum > MAX_LIST-2 && from != 0) from--; } if((kDown & KEY_DOWN || ((kHeld & KEY_DOWN) && (osGetTime() - mill > 500))) && fileNum < fileMax) { fileNum++; if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 && from < fileMax - MAX_LIST) from++; } if((kDown & KEY_LEFT || ((kHeld & KEY_LEFT) && (osGetTime() - mill > 500))) && fileNum > 0) { int skip = MAX_LIST / 2; if(fileNum < skip) skip = fileNum; fileNum -= skip; // one line taken up by cwd, other by ../ if(fileMax - fileNum > MAX_LIST-2 && from != 0) { from -= skip; if(from < 0) from = 0; } } if((kDown & KEY_RIGHT || ((kHeld & KEY_RIGHT) && (osGetTime() - mill > 500))) && fileNum < fileMax) { int skip = fileMax - fileNum; if(skip > MAX_LIST / 2) skip = MAX_LIST / 2; fileNum += skip; if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 && from < fileMax - MAX_LIST) { from += skip; if(from > fileMax - MAX_LIST) from = fileMax - MAX_LIST; } } /* * Pressing B goes up a folder, as well as pressing A or R when ".." * is selected. */ if((kDown & KEY_B) || ((kDown & KEY_A) && (from == 0 && fileNum == 0))) { chdir(".."); fileMax = getDir(&dirList); fileNum = prevPosition[0]; from = prevFrom[0]; for (int i=0; i= fileNum) { chdir(dirList.directories[fileNum - 1]); fileMax = getDir(&dirList); oldFileNum = fileNum; oldFrom = from; fileNum = 0; from = 0; /* save old position in folder */ for (int i=MAX_DIRECTORIES-1; i>0; i--) { prevPosition[i] = prevPosition[i-1]; prevFrom[i] = prevFrom[i-1]; } prevPosition[0] = oldFileNum; prevFrom[0] = oldFrom; continue; } if(dirList.dirNum < fileNum) { /* Extract and display metadata */ char fullPath[512]; snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]); extractMetadata(fullPath, ¤tMetadata); guiDisplayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]); changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo); error = 0; continue; } } // ignore R release if key combo with R used bool keyRActivation = false; if (kUp & KEY_R) { if (!keyRComboPressed) { keyRActivation = true; } keyRComboPressed = false; } bool goToNextFile = (kDown & KEY_ZR) || keyRActivation; // check that next entry is a file if (goToNextFile && fileNum < fileMax && dirList.dirNum < fileNum+1) { fileNum += 1; if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 && from < fileMax - MAX_LIST) from++; /* Extract and display metadata */ char fullPath[512]; snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]); extractMetadata(fullPath, ¤tMetadata); guiDisplayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]); changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo); error = 0; continue; } // ignore L release if key combo with L used bool keyLActivation = false; if (kUp & KEY_L) { if (!keyLComboPressed) { keyLActivation = true; } keyLComboPressed = false; } bool goToPrevFile = (kDown & KEY_ZL) || keyLActivation; // don't go to ../ and check that previous entry is a file if (goToPrevFile && fileNum > 1 && dirList.dirNum < fileNum-1) { fileNum -= 1; if(fileMax - fileNum > MAX_LIST-2 && from != 0) from--; /* Extract and display metadata */ char fullPath[512]; snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]); extractMetadata(fullPath, ¤tMetadata); guiDisplayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]); changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo); error = 0; continue; } // play next song automatically if (error == -1) { // don't try to play folders if (fileNum >= fileMax || dirList.dirNum >= fileNum) { error = 0; continue; } fileNum += 1; /* Extract and display metadata */ char fullPath[512]; snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]); extractMetadata(fullPath, ¤tMetadata); guiDisplayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]); changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo); error = 0; continue; } /* Render GUI elements */ const char** fileList; int fileListCount; buildFileListForGUI(dirList, &fileList, &fileListCount, 0); /* 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); } /* Calculate scroll position to keep selection visible (13 lines visible with path) */ int scroll = from; if(fileNum < scroll) scroll = fileNum; 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); /* Display logs on top screen */ guiDisplayLog((const char**)logMessages, logMessageCount, logScroll); /* 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); } /* Display version */ guiDisplayVersion(MICE_VERSION); /* End GUI frame */ guiEndFrame(); } out: addLogMessage("Exiting..."); runThreads = false; clearMetadata(¤tMetadata); svcSignalEvent(playbackFailEvent); changeFile(NULL, &playbackInfo); /* Cleanup GUI */ guiExit(); /* Cleanup log messages */ for(int i = 0; i < logMessageCount; i++) free(logMessages[i]); return 0; err: addLogMessage("A fatal error occurred. Press START to exit."); while(true) { u32 kDown; hidScanInput(); kDown = hidKeysDown(); if(kDown & KEY_START) break; } goto out; }