682 lines
16 KiB
C
682 lines
16 KiB
C
/**
|
|
* mice - 3DS Music Player
|
|
* Copyright (C) 2016 Mahyar Koshkouei
|
|
*
|
|
* 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 <dirent.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#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<MAX_DIRECTORIES-1; i++) {
|
|
prevPosition[i] = prevPosition[i+1];
|
|
prevFrom[i] = prevFrom[i+1];
|
|
}
|
|
/* default to first entry */
|
|
prevPosition[MAX_DIRECTORIES-1] = 0;
|
|
prevFrom[MAX_DIRECTORIES-1] = 0;
|
|
|
|
continue;
|
|
}
|
|
|
|
if(kDown & KEY_A)
|
|
{
|
|
if(dirList.dirNum >= 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 (14 lines visible) */
|
|
int scroll = from;
|
|
if(fileNum < scroll)
|
|
scroll = fileNum;
|
|
else if(fileNum >= scroll + 14)
|
|
scroll = fileNum - 13;
|
|
|
|
/* 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 */
|
|
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;
|
|
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;
|
|
}
|