Compare commits
4 Commits
ab9b766be3
...
dev50
| Author | SHA1 | Date | |
|---|---|---|---|
|
d09cf0739e
|
|||
|
8be23ca4fc
|
|||
|
5d9ce1fdb9
|
|||
|
e491cddfcb
|
4
Makefile
4
Makefile
@@ -49,9 +49,9 @@ SOURCE_DIRS := source
|
|||||||
EXTRA_OUTPUT_FILES :=
|
EXTRA_OUTPUT_FILES :=
|
||||||
|
|
||||||
LIBRARY_DIRS := $(DEVKITPRO)/libctru $(DEVKITPRO)/portlibs/armv6k $(DEVKITPRO)/portlibs/3ds
|
LIBRARY_DIRS := $(DEVKITPRO)/libctru $(DEVKITPRO)/portlibs/armv6k $(DEVKITPRO)/portlibs/3ds
|
||||||
LIBRARIES := 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)/portlibs/armv6k/include/opus -I$(DEVKITPRO)/portlibs/3ds/include/opus -O3 -g3 -ffunction-sections -fdata-sections
|
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
|
# -O0 -g3 -fstack-protector-strong -fsanitize=undefined -fsanitize-trap
|
||||||
RUN_FLAGS :=
|
RUN_FLAGS :=
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ The latest 3DSX/CIA/3DS download can be found on the <a href="https://github.com
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
* Plays PCM WAV, AIFF, FLAC, Opus, Vorbis and MP3 files.
|
* Plays PCM WAV, AIFF, FLAC, Opus, Vorbis and MP3 files.
|
||||||
|
* M4A/AAC/ALAC support (file detection implemented, decoder integration in progress).
|
||||||
* Pause and play support.
|
* Pause and play support.
|
||||||
* Plays music via headphones whilst system is closed.
|
* Plays music via headphones whilst system is closed.
|
||||||
* Ability to browse directories.
|
* Ability to browse directories.
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ enum file_types
|
|||||||
FILE_TYPE_VORBIS,
|
FILE_TYPE_VORBIS,
|
||||||
FILE_TYPE_OPUS,
|
FILE_TYPE_OPUS,
|
||||||
FILE_TYPE_MP3,
|
FILE_TYPE_MP3,
|
||||||
FILE_TYPE_SID
|
FILE_TYPE_SID,
|
||||||
|
FILE_TYPE_M4A,
|
||||||
|
FILE_TYPE_AAC
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
118
include/gui.h
Normal file
118
include/gui.h
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
#ifndef mice_gui_h
|
||||||
|
#define mice_gui_h
|
||||||
|
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <citro2d.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "metadata.h"
|
||||||
|
|
||||||
|
/* GUI color definitions */
|
||||||
|
#define GUI_COLOR_BG_TOP C2D_Color32(20, 20, 30, 255)
|
||||||
|
#define GUI_COLOR_BG_BOTTOM C2D_Color32(15, 15, 25, 255)
|
||||||
|
#define GUI_COLOR_TEXT C2D_Color32(255, 255, 255, 255)
|
||||||
|
#define GUI_COLOR_TEXT_DIM C2D_Color32(180, 180, 180, 255)
|
||||||
|
#define GUI_COLOR_ACCENT C2D_Color32(100, 150, 255, 255)
|
||||||
|
#define GUI_COLOR_HIGHLIGHT C2D_Color32(50, 80, 150, 255)
|
||||||
|
|
||||||
|
/* Screen dimensions */
|
||||||
|
#define TOP_SCREEN_WIDTH 400
|
||||||
|
#define TOP_SCREEN_HEIGHT 240
|
||||||
|
#define BOTTOM_SCREEN_WIDTH 320
|
||||||
|
#define BOTTOM_SCREEN_HEIGHT 240
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the GUI system
|
||||||
|
*
|
||||||
|
* \return 0 on success, -1 on failure
|
||||||
|
*/
|
||||||
|
int guiInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up and exit the GUI system
|
||||||
|
*/
|
||||||
|
void guiExit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin rendering a frame
|
||||||
|
*/
|
||||||
|
void guiBeginFrame(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End rendering a frame and display it
|
||||||
|
*/
|
||||||
|
void guiEndFrame(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the top screen
|
||||||
|
*/
|
||||||
|
void guiClearTopScreen(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the bottom screen
|
||||||
|
*/
|
||||||
|
void guiClearBottomScreen(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display metadata on the top screen
|
||||||
|
*
|
||||||
|
* \param metadata Pointer to metadata structure
|
||||||
|
* \param filename Filename to display if no title is available
|
||||||
|
*/
|
||||||
|
void guiDisplayMetadata(struct metadata_t* metadata, const char* filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display log messages on the top screen
|
||||||
|
*
|
||||||
|
* \param messages Array of message strings
|
||||||
|
* \param count Number of messages
|
||||||
|
* \param scroll Scroll offset for messages
|
||||||
|
*/
|
||||||
|
void guiDisplayLog(const char** messages, int count, int scroll);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display file list on the bottom screen
|
||||||
|
*
|
||||||
|
* \param files Array of filenames
|
||||||
|
* \param count Number of files
|
||||||
|
* \param selected Index of selected file
|
||||||
|
* \param scroll Scroll offset
|
||||||
|
*/
|
||||||
|
void guiDisplayFileList(const char** files, int count, int selected, int scroll);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display playback controls and status on the bottom screen
|
||||||
|
*
|
||||||
|
* \param isPlaying Whether playback is active
|
||||||
|
* \param isPaused Whether playback is paused
|
||||||
|
* \param position Current position in seconds
|
||||||
|
* \param duration Total duration in seconds
|
||||||
|
*/
|
||||||
|
void guiDisplayPlaybackStatus(bool isPlaying, bool isPaused, float position, float duration);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display version text
|
||||||
|
*
|
||||||
|
* \param version Version string to display
|
||||||
|
*/
|
||||||
|
void guiDisplayVersion(const char* version);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a simple text string at specified position
|
||||||
|
*
|
||||||
|
* \param screen Target screen (GFX_TOP or GFX_BOTTOM)
|
||||||
|
* \param x X coordinate
|
||||||
|
* \param y Y coordinate
|
||||||
|
* \param text Text to display
|
||||||
|
* \param color Text color
|
||||||
|
* \param scale Text scale (default 0.5f)
|
||||||
|
*/
|
||||||
|
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
|
||||||
21
include/m4a.h
Normal file
21
include/m4a.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#ifndef M4A_H
|
||||||
|
#define M4A_H
|
||||||
|
|
||||||
|
#include "playback.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set decoder parameters for M4A/AAC.
|
||||||
|
*
|
||||||
|
* \param decoder Structure to store parameters.
|
||||||
|
*/
|
||||||
|
void setM4a(struct decoder_fn* decoder);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file is an M4A/AAC/ALAC file.
|
||||||
|
*
|
||||||
|
* \param file File location.
|
||||||
|
* \return 0 on success, -1 on failure.
|
||||||
|
*/
|
||||||
|
int isM4a(const char* file);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
#define mice_main_h
|
#define mice_main_h
|
||||||
|
|
||||||
/* Application version */
|
/* Application version */
|
||||||
#define MICE_VERSION "dev28"
|
#define MICE_VERSION "dev50"
|
||||||
|
|
||||||
/* Default folder */
|
/* Default folder */
|
||||||
#define DEFAULT_DIR "sdmc:/"
|
#define DEFAULT_DIR "sdmc:/"
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ void stopPlayback(void);
|
|||||||
*/
|
*/
|
||||||
bool isPlaying(void);
|
bool isPlaying(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether playback is currently paused.
|
||||||
|
*/
|
||||||
|
bool isPaused(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should only be called from a new thread only, and have only one playback
|
* Should only be called from a new thread only, and have only one playback
|
||||||
* thread at time. This function has not been written for more than one
|
* thread at time. This function has not been written for more than one
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "error.h"
|
#include "error.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
#include "flac.h"
|
#include "flac.h"
|
||||||
|
#include "m4a.h"
|
||||||
#include "mp3.h"
|
#include "mp3.h"
|
||||||
#include "opus.h"
|
#include "opus.h"
|
||||||
#include "vorbis.h"
|
#include "vorbis.h"
|
||||||
@@ -26,7 +27,9 @@ const char* fileToStr(enum file_types ft)
|
|||||||
"VORBIS",
|
"VORBIS",
|
||||||
"OPUS",
|
"OPUS",
|
||||||
"MP3",
|
"MP3",
|
||||||
"SID"
|
"SID",
|
||||||
|
"M4A",
|
||||||
|
"AAC"
|
||||||
};
|
};
|
||||||
|
|
||||||
return file_types_str[ft];
|
return file_types_str[ft];
|
||||||
@@ -90,24 +93,37 @@ enum file_types getFileType(const char *file)
|
|||||||
file_type = FILE_TYPE_SID;
|
file_type = FILE_TYPE_SID;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
/*
|
/* Check for M4A/AAC/ALAC (MP4 container with ftyp atom) */
|
||||||
* MP3 without ID3 tag, ID3v1 tag is at the end of file, or MP3
|
if((fileSig == 0x70797466) || /* 'ftyp' at offset 4 */
|
||||||
* with ID3 tag at the beginning of the file.
|
(fileSig == 0x65657266)) /* 'free' at offset 4 (some M4A files) */
|
||||||
*/
|
{
|
||||||
if(isMp3(file) == 0)
|
file_type = FILE_TYPE_M4A;
|
||||||
{
|
|
||||||
file_type = FILE_TYPE_MP3;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TODO: Add this again at some point */
|
|
||||||
//printf("Unknown magic number: %#010x\n.", fileSig);
|
|
||||||
errno = FILE_NOT_SUPPORTED;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Check for raw AAC (ADTS format) - sync word 0xFFF */
|
||||||
|
if((fileSig & 0xFFF60000) == 0xFFF00000)
|
||||||
|
{
|
||||||
|
file_type = FILE_TYPE_AAC;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
err:
|
/*
|
||||||
|
* MP3 without ID3 tag, ID3v1 tag is at the end of file, or MP3
|
||||||
|
* with ID3 tag at the beginning of the file.
|
||||||
|
*/
|
||||||
|
if(isMp3(file) == 0)
|
||||||
|
{
|
||||||
|
file_type = FILE_TYPE_MP3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO: Add this again at some point */
|
||||||
|
//printf("Unknown magic number: %#010x\n.", fileSig);
|
||||||
|
errno = FILE_NOT_SUPPORTED;
|
||||||
|
break;
|
||||||
|
}err:
|
||||||
fclose(ftest);
|
fclose(ftest);
|
||||||
return file_type;
|
return file_type;
|
||||||
}
|
}
|
||||||
|
|||||||
466
source/gui.c
Normal file
466
source/gui.c
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
#include <3ds.h>
|
||||||
|
#include <citro2d.h>
|
||||||
|
#include <citro3d.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <png.h>
|
||||||
|
#include "gui.h"
|
||||||
|
#include "metadata.h"
|
||||||
|
|
||||||
|
static C3D_RenderTarget* topTarget = NULL;
|
||||||
|
static C3D_RenderTarget* bottomTarget = NULL;
|
||||||
|
static C2D_TextBuf textBuf = NULL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the GUI system
|
||||||
|
*/
|
||||||
|
int guiInit(void)
|
||||||
|
{
|
||||||
|
gfxInitDefault();
|
||||||
|
C3D_Init(C3D_DEFAULT_CMDBUF_SIZE);
|
||||||
|
C2D_Init(C2D_DEFAULT_MAX_OBJECTS);
|
||||||
|
C2D_Prepare();
|
||||||
|
|
||||||
|
/* Create render targets for top and bottom screens */
|
||||||
|
topTarget = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT);
|
||||||
|
bottomTarget = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT);
|
||||||
|
|
||||||
|
if(!topTarget || !bottomTarget)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
/* Create text buffer */
|
||||||
|
textBuf = C2D_TextBufNew(4096);
|
||||||
|
if(!textBuf)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up and exit the GUI system
|
||||||
|
*/
|
||||||
|
void guiExit(void)
|
||||||
|
{
|
||||||
|
if(textBuf)
|
||||||
|
C2D_TextBufDelete(textBuf);
|
||||||
|
|
||||||
|
C2D_Fini();
|
||||||
|
C3D_Fini();
|
||||||
|
gfxExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin rendering a frame
|
||||||
|
*/
|
||||||
|
void guiBeginFrame(void)
|
||||||
|
{
|
||||||
|
C3D_FrameBegin(C3D_FRAME_SYNCDRAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End rendering a frame and display it
|
||||||
|
*/
|
||||||
|
void guiEndFrame(void)
|
||||||
|
{
|
||||||
|
C3D_FrameEnd(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the top screen
|
||||||
|
*/
|
||||||
|
void guiClearTopScreen(void)
|
||||||
|
{
|
||||||
|
C2D_TargetClear(topTarget, GUI_COLOR_BG_TOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the bottom screen
|
||||||
|
*/
|
||||||
|
void guiClearBottomScreen(void)
|
||||||
|
{
|
||||||
|
C2D_TargetClear(bottomTarget, GUI_COLOR_BG_BOTTOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a simple text string at specified position
|
||||||
|
*/
|
||||||
|
void guiDrawText(gfxScreen_t screen, float x, float y, const char* text, u32 color, float scale)
|
||||||
|
{
|
||||||
|
if(!text || !textBuf)
|
||||||
|
return;
|
||||||
|
|
||||||
|
C2D_Text c2dText;
|
||||||
|
C2D_TextBufClear(textBuf);
|
||||||
|
C2D_TextParse(&c2dText, textBuf, text);
|
||||||
|
C2D_TextOptimize(&c2dText);
|
||||||
|
|
||||||
|
C3D_RenderTarget* target = (screen == GFX_TOP) ? topTarget : bottomTarget;
|
||||||
|
C2D_SceneBegin(target);
|
||||||
|
C2D_DrawText(&c2dText, C2D_WithColor, x, y, 0.5f, scale, scale, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display metadata on the top screen
|
||||||
|
*/
|
||||||
|
void guiDisplayMetadata(struct metadata_t* metadata, const char* filename)
|
||||||
|
{
|
||||||
|
if(!metadata || !filename || !textBuf)
|
||||||
|
return;
|
||||||
|
|
||||||
|
C2D_SceneBegin(topTarget);
|
||||||
|
|
||||||
|
/* Extract just the filename without path and extension for fallback */
|
||||||
|
const char* basename = strrchr(filename, '/');
|
||||||
|
if(!basename)
|
||||||
|
basename = filename;
|
||||||
|
else
|
||||||
|
basename++;
|
||||||
|
|
||||||
|
/* Remove file extension for display */
|
||||||
|
char displayName[64];
|
||||||
|
strncpy(displayName, basename, sizeof(displayName) - 1);
|
||||||
|
displayName[sizeof(displayName) - 1] = '\0';
|
||||||
|
char* dot = strrchr(displayName, '.');
|
||||||
|
if(dot) *dot = '\0';
|
||||||
|
|
||||||
|
C2D_Text text;
|
||||||
|
float y = 10.0f;
|
||||||
|
float scale = 0.6f;
|
||||||
|
float lineHeight = 20.0f;
|
||||||
|
|
||||||
|
C2D_TextBufClear(textBuf);
|
||||||
|
|
||||||
|
/* Draw title */
|
||||||
|
if(metadata->title[0])
|
||||||
|
{
|
||||||
|
char titleBuf[64];
|
||||||
|
snprintf(titleBuf, sizeof(titleBuf), "%.47s", metadata->title);
|
||||||
|
C2D_TextParse(&text, textBuf, titleBuf);
|
||||||
|
C2D_TextOptimize(&text);
|
||||||
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale, scale, GUI_COLOR_TEXT);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
char titleBuf[64];
|
||||||
|
snprintf(titleBuf, sizeof(titleBuf), "%.47s", displayName);
|
||||||
|
C2D_TextParse(&text, textBuf, titleBuf);
|
||||||
|
C2D_TextOptimize(&text);
|
||||||
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale, scale, GUI_COLOR_TEXT);
|
||||||
|
}
|
||||||
|
y += lineHeight;
|
||||||
|
|
||||||
|
/* Draw artist */
|
||||||
|
if(metadata->artist[0])
|
||||||
|
{
|
||||||
|
char artistBuf[64];
|
||||||
|
snprintf(artistBuf, sizeof(artistBuf), "%.45s", metadata->artist);
|
||||||
|
C2D_TextParse(&text, textBuf, artistBuf);
|
||||||
|
C2D_TextOptimize(&text);
|
||||||
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale * 0.8f, scale * 0.8f, GUI_COLOR_TEXT_DIM);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
C2D_TextParse(&text, textBuf, "Unknown Artist");
|
||||||
|
C2D_TextOptimize(&text);
|
||||||
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale * 0.8f, scale * 0.8f, GUI_COLOR_TEXT_DIM);
|
||||||
|
}
|
||||||
|
y += lineHeight;
|
||||||
|
|
||||||
|
/* Draw album */
|
||||||
|
if(metadata->album[0])
|
||||||
|
{
|
||||||
|
char albumBuf[64];
|
||||||
|
snprintf(albumBuf, sizeof(albumBuf), "%.45s", metadata->album);
|
||||||
|
C2D_TextParse(&text, textBuf, albumBuf);
|
||||||
|
C2D_TextOptimize(&text);
|
||||||
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale * 0.8f, scale * 0.8f, GUI_COLOR_TEXT_DIM);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
C2D_TextParse(&text, textBuf, "Unknown Album");
|
||||||
|
C2D_TextOptimize(&text);
|
||||||
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale * 0.8f, scale * 0.8f, GUI_COLOR_TEXT_DIM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display log messages on the top screen
|
||||||
|
*/
|
||||||
|
void guiDisplayLog(const char** messages, int count, int scroll)
|
||||||
|
{
|
||||||
|
if(!messages || count <= 0 || !textBuf)
|
||||||
|
return;
|
||||||
|
|
||||||
|
C2D_SceneBegin(topTarget);
|
||||||
|
|
||||||
|
C2D_Text text;
|
||||||
|
float y = 70.0f; /* Start below metadata area */
|
||||||
|
float scale = 0.4f;
|
||||||
|
float lineHeight = 12.0f;
|
||||||
|
int maxLines = 14;
|
||||||
|
|
||||||
|
C2D_TextBufClear(textBuf);
|
||||||
|
|
||||||
|
for(int i = scroll; i < count && (i - scroll) < maxLines; i++)
|
||||||
|
{
|
||||||
|
if(messages[i])
|
||||||
|
{
|
||||||
|
C2D_TextParse(&text, textBuf, messages[i]);
|
||||||
|
C2D_TextOptimize(&text);
|
||||||
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale, scale, GUI_COLOR_TEXT);
|
||||||
|
y += lineHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display file list on the bottom screen
|
||||||
|
*/
|
||||||
|
void guiDisplayFileList(const char** files, int count, int selected, int scroll)
|
||||||
|
{
|
||||||
|
if(!files || count <= 0 || !textBuf)
|
||||||
|
return;
|
||||||
|
|
||||||
|
C2D_SceneBegin(bottomTarget);
|
||||||
|
|
||||||
|
C2D_Text text;
|
||||||
|
float y = 10.0f;
|
||||||
|
float scale = 0.5f;
|
||||||
|
float lineHeight = 16.0f;
|
||||||
|
int maxLines = 14;
|
||||||
|
|
||||||
|
C2D_TextBufClear(textBuf);
|
||||||
|
|
||||||
|
for(int i = scroll; i < count && (i - scroll) < maxLines; i++)
|
||||||
|
{
|
||||||
|
if(files[i])
|
||||||
|
{
|
||||||
|
/* Check if this is a directory */
|
||||||
|
size_t len = strlen(files[i]);
|
||||||
|
bool isDir = (len > 0 && files[i][len-1] == '/');
|
||||||
|
|
||||||
|
/* Draw selection highlight */
|
||||||
|
if(i == selected)
|
||||||
|
{
|
||||||
|
C2D_DrawRectSolid(5.0f, y - 2.0f, 0.5f, 310.0f, lineHeight, GUI_COLOR_HIGHLIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Truncate if too long */
|
||||||
|
char displayName[48];
|
||||||
|
snprintf(displayName, sizeof(displayName), "%.40s", files[i]);
|
||||||
|
|
||||||
|
C2D_TextParse(&text, textBuf, displayName);
|
||||||
|
C2D_TextOptimize(&text);
|
||||||
|
|
||||||
|
/* Use different color for directories */
|
||||||
|
u32 color;
|
||||||
|
if(i == selected)
|
||||||
|
color = GUI_COLOR_ACCENT;
|
||||||
|
else if(isDir)
|
||||||
|
color = C2D_Color32(100, 200, 255, 255); /* Light blue for folders */
|
||||||
|
else
|
||||||
|
color = GUI_COLOR_TEXT;
|
||||||
|
|
||||||
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale, scale, color);
|
||||||
|
y += lineHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display playback controls and status on the top screen
|
||||||
|
*/
|
||||||
|
void guiDisplayPlaybackStatus(bool isPlaying, bool isPaused, float position, float duration)
|
||||||
|
{
|
||||||
|
if(!textBuf)
|
||||||
|
return;
|
||||||
|
|
||||||
|
C2D_SceneBegin(topTarget);
|
||||||
|
|
||||||
|
C2D_Text text;
|
||||||
|
C2D_TextBufClear(textBuf);
|
||||||
|
|
||||||
|
/* Display status and time at bottom of top screen */
|
||||||
|
float y = 215.0f;
|
||||||
|
|
||||||
|
/* Display status */
|
||||||
|
char statusBuf[64];
|
||||||
|
if(isPlaying)
|
||||||
|
{
|
||||||
|
if(isPaused)
|
||||||
|
snprintf(statusBuf, sizeof(statusBuf), "Paused");
|
||||||
|
else
|
||||||
|
snprintf(statusBuf, sizeof(statusBuf), "Playing");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
snprintf(statusBuf, sizeof(statusBuf), "Stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
C2D_TextParse(&text, textBuf, statusBuf);
|
||||||
|
C2D_TextOptimize(&text);
|
||||||
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, 0.5f, 0.5f, GUI_COLOR_TEXT);
|
||||||
|
|
||||||
|
/* Display time if playing */
|
||||||
|
if(isPlaying && duration > 0)
|
||||||
|
{
|
||||||
|
char timeBuf[32];
|
||||||
|
int posMin = (int)position / 60;
|
||||||
|
int posSec = (int)position % 60;
|
||||||
|
int durMin = (int)duration / 60;
|
||||||
|
int durSec = (int)duration % 60;
|
||||||
|
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d / %02d:%02d", posMin, posSec, durMin, durSec);
|
||||||
|
|
||||||
|
C2D_TextParse(&text, textBuf, timeBuf);
|
||||||
|
C2D_TextOptimize(&text);
|
||||||
|
C2D_DrawText(&text, C2D_WithColor, 280.0f, y, 0.5f, 0.5f, 0.5f, GUI_COLOR_TEXT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display version text and credits at bottom of bottom screen
|
||||||
|
*/
|
||||||
|
void guiDisplayVersion(const char* version)
|
||||||
|
{
|
||||||
|
if(!textBuf)
|
||||||
|
return;
|
||||||
|
|
||||||
|
C2D_SceneBegin(bottomTarget);
|
||||||
|
|
||||||
|
C2D_Text text;
|
||||||
|
C2D_TextBufClear(textBuf);
|
||||||
|
|
||||||
|
/* Display "mice - by sillyangel" at bottom center */
|
||||||
|
const char* 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
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
133
source/m4a.c
Normal file
133
source/m4a.c
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <3ds.h>
|
||||||
|
|
||||||
|
#include "error.h"
|
||||||
|
#include "m4a.h"
|
||||||
|
#include "playback.h"
|
||||||
|
|
||||||
|
/* TODO: Integrate proper AAC decoder library (libfaad2, fdk-aac, or minimp4) */
|
||||||
|
/* For now, this is a stub implementation */
|
||||||
|
|
||||||
|
static size_t* buffSize;
|
||||||
|
static uint32_t rate = 44100;
|
||||||
|
static uint8_t channels = 2;
|
||||||
|
|
||||||
|
static int initM4a(const char* file);
|
||||||
|
static uint32_t rateM4a(void);
|
||||||
|
static uint8_t channelM4a(void);
|
||||||
|
static uint64_t decodeM4a(void* buffer);
|
||||||
|
static void exitM4a(void);
|
||||||
|
static size_t getFileSamplesM4a(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set decoder parameters for M4A/AAC.
|
||||||
|
*
|
||||||
|
* \param decoder Structure to store parameters.
|
||||||
|
*/
|
||||||
|
void setM4a(struct decoder_fn* decoder)
|
||||||
|
{
|
||||||
|
decoder->init = &initM4a;
|
||||||
|
decoder->rate = &rateM4a;
|
||||||
|
decoder->channels = &channelM4a;
|
||||||
|
buffSize = &(decoder->buffSize);
|
||||||
|
decoder->decode = &decodeM4a;
|
||||||
|
decoder->exit = &exitM4a;
|
||||||
|
decoder->getFileSamples = &getFileSamplesM4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file is an M4A/AAC/ALAC file.
|
||||||
|
*
|
||||||
|
* \param file File location.
|
||||||
|
* \return 0 on success, -1 on failure.
|
||||||
|
*/
|
||||||
|
int isM4a(const char* file)
|
||||||
|
{
|
||||||
|
FILE* ftest = fopen(file, "rb");
|
||||||
|
uint32_t fileSig;
|
||||||
|
uint32_t ftypSig;
|
||||||
|
|
||||||
|
if(ftest == NULL)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
/* Read first 4 bytes (should be size of ftyp atom) */
|
||||||
|
if(fread(&fileSig, 4, 1, ftest) == 0)
|
||||||
|
{
|
||||||
|
fclose(ftest);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read next 4 bytes (should be 'ftyp') */
|
||||||
|
if(fread(&ftypSig, 4, 1, ftest) == 0)
|
||||||
|
{
|
||||||
|
fclose(ftest);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(ftest);
|
||||||
|
|
||||||
|
/* Check for 'ftyp' signature (0x70797466 in little-endian) */
|
||||||
|
if(ftypSig == 0x70797466)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int initM4a(const char* file)
|
||||||
|
{
|
||||||
|
(void)file;
|
||||||
|
|
||||||
|
/* TODO: Initialize AAC decoder */
|
||||||
|
/* This requires:
|
||||||
|
* 1. Parse MP4 container to find AAC audio track
|
||||||
|
* 2. Extract decoder config (sample rate, channels, etc.)
|
||||||
|
* 3. Initialize AAC decoder with config
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Set default values for now */
|
||||||
|
rate = 44100;
|
||||||
|
channels = 2;
|
||||||
|
*buffSize = rate * channels * sizeof(int16_t);
|
||||||
|
|
||||||
|
errno = FILE_NOT_SUPPORTED;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t rateM4a(void)
|
||||||
|
{
|
||||||
|
return rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t channelM4a(void)
|
||||||
|
{
|
||||||
|
return channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t decodeM4a(void* buffer)
|
||||||
|
{
|
||||||
|
(void)buffer;
|
||||||
|
|
||||||
|
/* TODO: Decode AAC frame */
|
||||||
|
/* This requires:
|
||||||
|
* 1. Read next AAC frame from MP4 container
|
||||||
|
* 2. Decode AAC frame to PCM samples
|
||||||
|
* 3. Write PCM samples to buffer
|
||||||
|
* 4. Return number of samples decoded
|
||||||
|
*/
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void exitM4a(void)
|
||||||
|
{
|
||||||
|
/* TODO: Clean up AAC decoder */
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t getFileSamplesM4a(void)
|
||||||
|
{
|
||||||
|
/* TODO: Calculate total samples from MP4 metadata */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
361
source/main.c
361
source/main.c
@@ -21,23 +21,31 @@
|
|||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "metadata.h"
|
#include "metadata.h"
|
||||||
#include "playback.h"
|
#include "playback.h"
|
||||||
|
#include "gui.h"
|
||||||
|
|
||||||
volatile bool runThreads = true;
|
volatile bool runThreads = true;
|
||||||
|
|
||||||
/**
|
/* Log message buffer for GUI display */
|
||||||
* Prints the current key mappings to stdio.
|
#define MAX_LOG_MESSAGES 100
|
||||||
*/
|
static char* logMessages[MAX_LOG_MESSAGES] = {0};
|
||||||
static void showControls(void)
|
static int logMessageCount = 0;
|
||||||
{
|
static int logScroll = 0;
|
||||||
printf("Button mappings:\n"
|
|
||||||
"Pause: L+R or L+Up\n"
|
static void addLogMessage(const char* msg) {
|
||||||
"Previous/Next Song: ZL/ZR or L/R\n"
|
if (logMessageCount >= MAX_LOG_MESSAGES) {
|
||||||
"A: Open File\n"
|
/* Remove oldest message */
|
||||||
"B: Go up folder\n"
|
free(logMessages[0]);
|
||||||
"Start: Exit\n"
|
memmove(logMessages, logMessages + 1, (MAX_LOG_MESSAGES - 1) * sizeof(char*));
|
||||||
"Browse: Up, Down, Left or Right\n");
|
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
|
* Allows the playback thread to return any error messages that it may
|
||||||
* encounter.
|
* encounter.
|
||||||
@@ -56,19 +64,17 @@ void playbackWatchdog(void* infoIn)
|
|||||||
|
|
||||||
if(*info->errInfo->error > 0)
|
if(*info->errInfo->error > 0)
|
||||||
{
|
{
|
||||||
continue;
|
char errorMsg[256];
|
||||||
consoleSelect(info->screen);
|
snprintf(errorMsg, sizeof(errorMsg), "Error %d: %s",
|
||||||
printf("Error %d: %s\n", *info->errInfo->error,
|
*info->errInfo->error, mice_strerror(*info->errInfo->error));
|
||||||
mice_strerror(*info->errInfo->error));
|
addLogMessage(errorMsg);
|
||||||
}
|
}
|
||||||
else if (*info->errInfo->error == -1)
|
else if (*info->errInfo->error == -1)
|
||||||
{
|
{
|
||||||
continue;
|
|
||||||
/* Used to signify that playback has stopped.
|
/* Used to signify that playback has stopped.
|
||||||
* Not technically an error.
|
* Not technically an error. Don't spam logs.
|
||||||
*/
|
*/
|
||||||
consoleSelect(info->screen);
|
/* addLogMessage("Stopped"); */
|
||||||
puts("Stopped");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +121,7 @@ static int changeFile(const char* ep_file, struct playbackInfo_t* playbackInfo)
|
|||||||
//playbackInfo->file = strdup(ep_file);
|
//playbackInfo->file = strdup(ep_file);
|
||||||
if (memccpy(playbackInfo->file, ep_file, '\0', sizeof(playbackInfo->file)) == NULL)
|
if (memccpy(playbackInfo->file, ep_file, '\0', sizeof(playbackInfo->file)) == NULL)
|
||||||
{
|
{
|
||||||
puts("Error: File path too long\n");
|
addLogMessage("Error: File path too long");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +170,7 @@ static int getDir(struct dirList_t* dirList)
|
|||||||
free(dirList->currentDir);
|
free(dirList->currentDir);
|
||||||
|
|
||||||
if((dirList->currentDir = strdup(wd)) == NULL)
|
if((dirList->currentDir = strdup(wd)) == NULL)
|
||||||
puts("Failure");
|
addLogMessage("Memory allocation failure");
|
||||||
|
|
||||||
if((dp = opendir(wd)) == NULL)
|
if((dp = opendir(wd)) == NULL)
|
||||||
goto out;
|
goto out;
|
||||||
@@ -177,7 +183,7 @@ static int getDir(struct dirList_t* dirList)
|
|||||||
dirList->directories = realloc(dirList->directories, (dirNum + 1) * sizeof(char*));
|
dirList->directories = realloc(dirList->directories, (dirNum + 1) * sizeof(char*));
|
||||||
|
|
||||||
if((dirList->directories[dirNum] = strdup(ep->d_name)) == NULL)
|
if((dirList->directories[dirNum] = strdup(ep->d_name)) == NULL)
|
||||||
puts("Failure");
|
addLogMessage("Memory allocation failure");
|
||||||
|
|
||||||
dirNum++;
|
dirNum++;
|
||||||
continue;
|
continue;
|
||||||
@@ -187,7 +193,7 @@ static int getDir(struct dirList_t* dirList)
|
|||||||
dirList->files = realloc(dirList->files, (fileNum + 1) * sizeof(char*));
|
dirList->files = realloc(dirList->files, (fileNum + 1) * sizeof(char*));
|
||||||
|
|
||||||
if((dirList->files[fileNum] = strdup(ep->d_name)) == NULL)
|
if((dirList->files[fileNum] = strdup(ep->d_name)) == NULL)
|
||||||
puts("Failure");
|
addLogMessage("Memory allocation failure");
|
||||||
|
|
||||||
fileNum++;
|
fileNum++;
|
||||||
}
|
}
|
||||||
@@ -207,59 +213,53 @@ out:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List current directory.
|
* Build file list for GUI display.
|
||||||
*
|
* Creates a combined list of directories and files for rendering.
|
||||||
* \param from First entry in directory to list.
|
|
||||||
* \param max Maximum number of entries to list. Must be > 0.
|
|
||||||
* \param select File to show as selected. Must be > 0.
|
|
||||||
* \return Number of entries listed or negative on error.
|
|
||||||
*/
|
*/
|
||||||
static int listDir(int from, int max, int select, struct dirList_t dirList)
|
static void buildFileListForGUI(struct dirList_t dirList, const char*** outList, int* outCount, int from)
|
||||||
{
|
{
|
||||||
int fileNum = 0;
|
static const char* combinedList[512];
|
||||||
int listed = 0;
|
static char entryBuffer[512][256];
|
||||||
|
int index = 0;
|
||||||
printf("\033[0;0H");
|
|
||||||
printf("Dir: %.33s\n", dirList.currentDir);
|
/* Add parent directory option */
|
||||||
|
|
||||||
if(from == 0)
|
if(from == 0)
|
||||||
{
|
{
|
||||||
printf("\33[2K%c../\n", select == 0 ? '>' : ' ');
|
snprintf(entryBuffer[index], sizeof(entryBuffer[index]), "../");
|
||||||
listed++;
|
combinedList[index] = entryBuffer[index];
|
||||||
max--;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
while(dirList.fileNum + dirList.dirNum > fileNum)
|
/* Add all directories */
|
||||||
|
for(int i = 0; i < dirList.dirNum && index < 512; i++)
|
||||||
{
|
{
|
||||||
fileNum++;
|
snprintf(entryBuffer[index], sizeof(entryBuffer[index]), "%s/", dirList.directories[i]);
|
||||||
|
combinedList[index] = entryBuffer[index];
|
||||||
if(fileNum <= from)
|
index++;
|
||||||
continue;
|
|
||||||
|
|
||||||
listed++;
|
|
||||||
|
|
||||||
if(dirList.dirNum >= fileNum)
|
|
||||||
{
|
|
||||||
printf("\33[2K%c\x1b[34;1m%.37s/\x1b[0m\n",
|
|
||||||
select == fileNum ? '>' : ' ',
|
|
||||||
dirList.directories[fileNum - 1]);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* fileNum must be referring to a file instead of a directory. */
|
|
||||||
if(dirList.dirNum < fileNum)
|
|
||||||
{
|
|
||||||
printf("\33[2K%c%.37s\n",
|
|
||||||
select == fileNum ? '>' : ' ',
|
|
||||||
dirList.files[fileNum - dirList.dirNum - 1]);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if(fileNum == max + from)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
return listed;
|
/**
|
||||||
|
* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -290,13 +290,12 @@ err:
|
|||||||
goto out;
|
goto out;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc __attribute__((unused)), char **argv __attribute__((unused)))
|
||||||
{
|
{
|
||||||
PrintConsole topScreenLog, topScreenInfo, bottomScreen;
|
|
||||||
int fileMax;
|
int fileMax;
|
||||||
int fileNum = 0;
|
int fileNum = 0;
|
||||||
int from = 0;
|
int from = 0;
|
||||||
Thread watchdogThread;
|
Thread watchdogThread __attribute__((unused));
|
||||||
Handle playbackFailEvent;
|
Handle playbackFailEvent;
|
||||||
struct watchdogInfo watchdogInfoIn;
|
struct watchdogInfo watchdogInfoIn;
|
||||||
struct errInfo_t errInfo;
|
struct errInfo_t errInfo;
|
||||||
@@ -309,26 +308,17 @@ int main(int argc, char **argv)
|
|||||||
bool keyLComboPressed = false;
|
bool keyLComboPressed = false;
|
||||||
bool keyRComboPressed = false;
|
bool keyRComboPressed = false;
|
||||||
|
|
||||||
gfxInitDefault();
|
/* Initialize GUI system */
|
||||||
consoleInit(GFX_TOP, &topScreenLog);
|
if(guiInit() != 0)
|
||||||
consoleInit(GFX_TOP, &topScreenInfo);
|
{
|
||||||
consoleInit(GFX_BOTTOM, &bottomScreen);
|
return -1;
|
||||||
|
}
|
||||||
/* Set console sizes. */
|
|
||||||
// (y-1) + (height) <= 30 (top screen only fits 30 lines)
|
|
||||||
consoleSetWindow(&topScreenLog, 1, 4, 50, 27);
|
|
||||||
consoleSetWindow(&topScreenInfo, 1, 1, 50, 3);
|
|
||||||
|
|
||||||
consoleSelect(&bottomScreen);
|
|
||||||
|
|
||||||
/* Display version in bottom right corner */
|
|
||||||
printf("\033[28;30H%s", MICE_VERSION);
|
|
||||||
|
|
||||||
svcCreateEvent(&playbackFailEvent, RESET_ONESHOT);
|
svcCreateEvent(&playbackFailEvent, RESET_ONESHOT);
|
||||||
errInfo.error = &error;
|
errInfo.error = &error;
|
||||||
errInfo.failEvent = &playbackFailEvent;
|
errInfo.failEvent = &playbackFailEvent;
|
||||||
|
|
||||||
watchdogInfoIn.screen = &topScreenLog;
|
watchdogInfoIn.screen = NULL; /* No longer using console */
|
||||||
watchdogInfoIn.errInfo = &errInfo;
|
watchdogInfoIn.errInfo = &errInfo;
|
||||||
watchdogThread = threadCreate(playbackWatchdog,
|
watchdogThread = threadCreate(playbackWatchdog,
|
||||||
&watchdogInfoIn, 4 * 1024, 0x20, -2, true);
|
&watchdogInfoIn, 4 * 1024, 0x20, -2, true);
|
||||||
@@ -346,13 +336,7 @@ int main(int argc, char **argv)
|
|||||||
/* TODO: Not actually possible to get less than 0 */
|
/* TODO: Not actually possible to get less than 0 */
|
||||||
if(getDir(&dirList) < 0)
|
if(getDir(&dirList) < 0)
|
||||||
{
|
{
|
||||||
puts("Unable to obtain directory information");
|
addLogMessage("Unable to obtain directory information");
|
||||||
goto err;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(listDir(from, MAX_LIST, 0, dirList) < 0)
|
|
||||||
{
|
|
||||||
err_print("Unable to list directory.");
|
|
||||||
goto err;
|
goto err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,25 +355,22 @@ int main(int argc, char **argv)
|
|||||||
u32 kUp;
|
u32 kUp;
|
||||||
static u64 mill = 0;
|
static u64 mill = 0;
|
||||||
|
|
||||||
gfxFlushBuffers();
|
/* Begin GUI frame */
|
||||||
gspWaitForVBlank();
|
guiBeginFrame();
|
||||||
gfxSwapBuffers();
|
guiClearTopScreen();
|
||||||
|
guiClearBottomScreen();
|
||||||
|
|
||||||
hidScanInput();
|
hidScanInput();
|
||||||
kDown = hidKeysDown();
|
kDown = hidKeysDown();
|
||||||
kHeld = hidKeysHeld();
|
kHeld = hidKeysHeld();
|
||||||
kUp = hidKeysUp();
|
kUp = hidKeysUp();
|
||||||
|
|
||||||
consoleSelect(&bottomScreen);
|
|
||||||
|
|
||||||
/* Exit mice */
|
/* Exit mice */
|
||||||
if(kDown & KEY_START)
|
if(kDown & KEY_START)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
consoleSelect(&topScreenLog);
|
/* Debug info logged if needed */
|
||||||
printf("\rNum: %d, Max: %d, from: %d ", fileNum, fileMax, from);
|
|
||||||
consoleSelect(&bottomScreen);
|
|
||||||
#endif
|
#endif
|
||||||
if(kDown)
|
if(kDown)
|
||||||
mill = osGetTime();
|
mill = osGetTime();
|
||||||
@@ -402,11 +383,7 @@ int main(int argc, char **argv)
|
|||||||
if(isPlaying() == false)
|
if(isPlaying() == false)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
consoleSelect(&topScreenLog);
|
togglePlayback();
|
||||||
if(togglePlayback() == true)
|
|
||||||
puts("Paused");
|
|
||||||
else
|
|
||||||
puts("Playing");
|
|
||||||
|
|
||||||
keyLComboPressed = true;
|
keyLComboPressed = true;
|
||||||
// distinguish between L+R and L+Up
|
// distinguish between L+R and L+Up
|
||||||
@@ -416,11 +393,9 @@ int main(int argc, char **argv)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Show controls */
|
/* Show controls - no longer needed with GUI */
|
||||||
if(kDown & KEY_LEFT)
|
if(kDown & KEY_LEFT)
|
||||||
{
|
{
|
||||||
consoleSelect(&topScreenLog);
|
|
||||||
showControls();
|
|
||||||
keyLComboPressed = true;
|
keyLComboPressed = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -428,14 +403,10 @@ int main(int argc, char **argv)
|
|||||||
// if R is pressed first
|
// if R is pressed first
|
||||||
if ((kHeld & KEY_R) && (kDown & KEY_L))
|
if ((kHeld & KEY_R) && (kDown & KEY_L))
|
||||||
{
|
{
|
||||||
if(isPlaying() == false)
|
if(isPlaying() == false)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
consoleSelect(&topScreenLog);
|
togglePlayback();
|
||||||
if(togglePlayback() == true)
|
|
||||||
puts("Paused");
|
|
||||||
else
|
|
||||||
puts("Playing");
|
|
||||||
|
|
||||||
keyLComboPressed = true;
|
keyLComboPressed = true;
|
||||||
keyRComboPressed = true;
|
keyRComboPressed = true;
|
||||||
@@ -451,9 +422,6 @@ int main(int argc, char **argv)
|
|||||||
// one line taken up by cwd, other by ../
|
// one line taken up by cwd, other by ../
|
||||||
if(fileMax - fileNum > MAX_LIST-2 && from != 0)
|
if(fileMax - fileNum > MAX_LIST-2 && from != 0)
|
||||||
from--;
|
from--;
|
||||||
|
|
||||||
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
|
|
||||||
err_print("Unable to list directory.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if((kDown & KEY_DOWN ||
|
if((kDown & KEY_DOWN ||
|
||||||
@@ -465,9 +433,6 @@ int main(int argc, char **argv)
|
|||||||
if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 &&
|
if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 &&
|
||||||
from < fileMax - MAX_LIST)
|
from < fileMax - MAX_LIST)
|
||||||
from++;
|
from++;
|
||||||
|
|
||||||
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
|
|
||||||
err_print("Unable to list directory.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if((kDown & KEY_LEFT ||
|
if((kDown & KEY_LEFT ||
|
||||||
@@ -485,12 +450,9 @@ int main(int argc, char **argv)
|
|||||||
if(fileMax - fileNum > MAX_LIST-2 && from != 0)
|
if(fileMax - fileNum > MAX_LIST-2 && from != 0)
|
||||||
{
|
{
|
||||||
from -= skip;
|
from -= skip;
|
||||||
if(from < 0)
|
if(from < 0)
|
||||||
from = 0;
|
from = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
|
|
||||||
err_print("Unable to list directory.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if((kDown & KEY_RIGHT ||
|
if((kDown & KEY_RIGHT ||
|
||||||
@@ -511,9 +473,6 @@ int main(int argc, char **argv)
|
|||||||
if(from > fileMax - MAX_LIST)
|
if(from > fileMax - MAX_LIST)
|
||||||
from = fileMax - MAX_LIST;
|
from = fileMax - MAX_LIST;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
|
|
||||||
err_print("Unable to list directory.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -524,7 +483,6 @@ int main(int argc, char **argv)
|
|||||||
((kDown & KEY_A) && (from == 0 && fileNum == 0)))
|
((kDown & KEY_A) && (from == 0 && fileNum == 0)))
|
||||||
{
|
{
|
||||||
chdir("..");
|
chdir("..");
|
||||||
consoleClear();
|
|
||||||
fileMax = getDir(&dirList);
|
fileMax = getDir(&dirList);
|
||||||
|
|
||||||
fileNum = prevPosition[0];
|
fileNum = prevPosition[0];
|
||||||
@@ -537,9 +495,6 @@ int main(int argc, char **argv)
|
|||||||
prevPosition[MAX_DIRECTORIES-1] = 0;
|
prevPosition[MAX_DIRECTORIES-1] = 0;
|
||||||
prevFrom[MAX_DIRECTORIES-1] = 0;
|
prevFrom[MAX_DIRECTORIES-1] = 0;
|
||||||
|
|
||||||
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
|
|
||||||
err_print("Unable to list directory.");
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,7 +503,6 @@ int main(int argc, char **argv)
|
|||||||
if(dirList.dirNum >= fileNum)
|
if(dirList.dirNum >= fileNum)
|
||||||
{
|
{
|
||||||
chdir(dirList.directories[fileNum - 1]);
|
chdir(dirList.directories[fileNum - 1]);
|
||||||
consoleClear();
|
|
||||||
fileMax = getDir(&dirList);
|
fileMax = getDir(&dirList);
|
||||||
|
|
||||||
oldFileNum = fileNum;
|
oldFileNum = fileNum;
|
||||||
@@ -556,36 +510,23 @@ int main(int argc, char **argv)
|
|||||||
fileNum = 0;
|
fileNum = 0;
|
||||||
from = 0;
|
from = 0;
|
||||||
|
|
||||||
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
|
/* save old position in folder */
|
||||||
{
|
for (int i=MAX_DIRECTORIES-1; i>0; i--) {
|
||||||
err_print("Unable to list directory.");
|
prevPosition[i] = prevPosition[i-1];
|
||||||
}
|
prevFrom[i] = prevFrom[i-1];
|
||||||
else
|
|
||||||
{
|
|
||||||
/* 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;
|
|
||||||
}
|
}
|
||||||
|
prevPosition[0] = oldFileNum;
|
||||||
|
prevFrom[0] = oldFrom;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(dirList.dirNum < fileNum)
|
if(dirList.dirNum < fileNum)
|
||||||
{
|
{
|
||||||
consoleSelect(&topScreenInfo);
|
|
||||||
consoleClear();
|
|
||||||
|
|
||||||
/* Extract and display metadata */
|
/* Extract and display metadata */
|
||||||
char fullPath[512];
|
char fullPath[512];
|
||||||
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
|
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
|
||||||
extractMetadata(fullPath, ¤tMetadata);
|
extractMetadata(fullPath, ¤tMetadata);
|
||||||
displayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
|
guiDisplayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
|
||||||
|
|
||||||
consoleSelect(&topScreenLog);
|
|
||||||
//consoleClear();
|
|
||||||
|
|
||||||
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
|
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
|
||||||
error = 0;
|
error = 0;
|
||||||
@@ -608,21 +549,15 @@ int main(int argc, char **argv)
|
|||||||
if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 &&
|
if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 &&
|
||||||
from < fileMax - MAX_LIST)
|
from < fileMax - MAX_LIST)
|
||||||
from++;
|
from++;
|
||||||
consoleSelect(&topScreenInfo);
|
|
||||||
consoleClear();
|
|
||||||
|
|
||||||
/* Extract and display metadata */
|
/* Extract and display metadata */
|
||||||
char fullPath[512];
|
char fullPath[512];
|
||||||
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
|
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
|
||||||
extractMetadata(fullPath, ¤tMetadata);
|
extractMetadata(fullPath, ¤tMetadata);
|
||||||
displayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
|
guiDisplayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
|
||||||
|
|
||||||
consoleSelect(&topScreenLog);
|
|
||||||
//consoleClear();
|
|
||||||
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
|
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
|
||||||
error = 0;
|
error = 0;
|
||||||
consoleSelect(&bottomScreen);
|
|
||||||
if(listDir(from, MAX_LIST, fileNum, dirList) < 0) err_print("Unable to list directory.");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// ignore L release if key combo with L used
|
// ignore L release if key combo with L used
|
||||||
@@ -639,21 +574,15 @@ int main(int argc, char **argv)
|
|||||||
fileNum -= 1;
|
fileNum -= 1;
|
||||||
if(fileMax - fileNum > MAX_LIST-2 && from != 0)
|
if(fileMax - fileNum > MAX_LIST-2 && from != 0)
|
||||||
from--;
|
from--;
|
||||||
consoleSelect(&topScreenInfo);
|
|
||||||
consoleClear();
|
|
||||||
|
|
||||||
/* Extract and display metadata */
|
/* Extract and display metadata */
|
||||||
char fullPath[512];
|
char fullPath[512];
|
||||||
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
|
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
|
||||||
extractMetadata(fullPath, ¤tMetadata);
|
extractMetadata(fullPath, ¤tMetadata);
|
||||||
displayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
|
guiDisplayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
|
||||||
|
|
||||||
consoleSelect(&topScreenLog);
|
|
||||||
//consoleClear();
|
|
||||||
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
|
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
|
||||||
error = 0;
|
error = 0;
|
||||||
consoleSelect(&bottomScreen);
|
|
||||||
if(listDir(from, MAX_LIST, fileNum, dirList) < 0) err_print("Unable to list directory.");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,78 +594,78 @@ int main(int argc, char **argv)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
fileNum += 1;
|
fileNum += 1;
|
||||||
consoleSelect(&topScreenInfo);
|
|
||||||
consoleClear();
|
|
||||||
|
|
||||||
/* Extract and display metadata */
|
/* Extract and display metadata */
|
||||||
char fullPath[512];
|
char fullPath[512];
|
||||||
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
|
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
|
||||||
extractMetadata(fullPath, ¤tMetadata);
|
extractMetadata(fullPath, ¤tMetadata);
|
||||||
displayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
|
guiDisplayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
|
||||||
|
|
||||||
consoleSelect(&topScreenLog);
|
|
||||||
//consoleClear();
|
|
||||||
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
|
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
|
||||||
error = 0;
|
error = 0;
|
||||||
consoleSelect(&bottomScreen);
|
|
||||||
if(listDir(from, MAX_LIST, fileNum, dirList) < 0) err_print("Unable to list directory.");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* After 1000ms, update playback time. */
|
/* Render GUI elements */
|
||||||
while(osGetTime() - mill > 1000)
|
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])
|
||||||
{
|
{
|
||||||
consoleSelect(&topScreenLog);
|
const char* currentFile = (fileNum > 0 && fileNum <= dirList.dirNum + dirList.fileNum) ?
|
||||||
/* Position cursor at bottom of log area for time display */
|
(fileNum > dirList.dirNum ? dirList.files[fileNum - dirList.dirNum - 1] : "..") : "";
|
||||||
printf("\033[29;0H\033[K"); /* Move to line 29, clear line */
|
guiDisplayMetadata(¤tMetadata, currentFile);
|
||||||
|
guiDisplayAlbumArt(¤tMetadata);
|
||||||
/* Avoid divide by zero. */
|
|
||||||
if(playbackInfo.samples_per_second == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
{
|
|
||||||
unsigned hr, min, sec;
|
|
||||||
size_t seconds_played;
|
|
||||||
|
|
||||||
seconds_played = playbackInfo.samples_played / playbackInfo.samples_per_second;
|
|
||||||
|
|
||||||
hr = (seconds_played/3600);
|
|
||||||
min = (seconds_played - (3600*hr))/60;
|
|
||||||
sec = (seconds_played -(3600*hr)-(min*60));
|
|
||||||
|
|
||||||
printf("%02d:%02d:%02d", hr, min, sec);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(playbackInfo.samples_total != 0)
|
|
||||||
{
|
|
||||||
unsigned hr, min, sec;
|
|
||||||
size_t seconds_total;
|
|
||||||
|
|
||||||
seconds_total = playbackInfo.samples_total / playbackInfo.samples_per_second;
|
|
||||||
|
|
||||||
hr = (seconds_total/3600);
|
|
||||||
min = (seconds_total - (3600*hr))/60;
|
|
||||||
sec = (seconds_total -(3600*hr)-(min*60));
|
|
||||||
|
|
||||||
printf(" %02d:%02d:%02d", hr, min, sec);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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:
|
out:
|
||||||
puts("Exiting...");
|
addLogMessage("Exiting...");
|
||||||
runThreads = false;
|
runThreads = false;
|
||||||
clearMetadata(¤tMetadata);
|
clearMetadata(¤tMetadata);
|
||||||
svcSignalEvent(playbackFailEvent);
|
svcSignalEvent(playbackFailEvent);
|
||||||
changeFile(NULL, &playbackInfo);
|
changeFile(NULL, &playbackInfo);
|
||||||
|
|
||||||
gfxExit();
|
/* Cleanup GUI */
|
||||||
|
guiExit();
|
||||||
|
|
||||||
|
/* Cleanup log messages */
|
||||||
|
for(int i = 0; i < logMessageCount; i++)
|
||||||
|
free(logMessages[i]);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
err:
|
err:
|
||||||
puts("A fatal error occurred. Press START to exit.");
|
addLogMessage("A fatal error occurred. Press START to exit.");
|
||||||
|
|
||||||
while(true)
|
while(true)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "metadata.h"
|
#include "metadata.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
#include "all.h"
|
#include "all.h"
|
||||||
|
#include "gui.h"
|
||||||
|
|
||||||
/* Internal helper functions */
|
/* Internal helper functions */
|
||||||
static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata);
|
static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata);
|
||||||
@@ -96,56 +97,11 @@ void clearMetadata(struct metadata_t* metadata)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Display metadata on the top screen
|
* Display metadata on the top screen
|
||||||
|
* (Now uses GUI rendering - this is a wrapper for compatibility)
|
||||||
*/
|
*/
|
||||||
void displayMetadata(struct metadata_t* metadata, const char* filename)
|
void displayMetadata(struct metadata_t* metadata, const char* filename)
|
||||||
{
|
{
|
||||||
if(!metadata || !filename)
|
guiDisplayMetadata(metadata, filename);
|
||||||
return;
|
|
||||||
|
|
||||||
/* Clear the top screen info area */
|
|
||||||
consoleClear();
|
|
||||||
|
|
||||||
/* Extract just the filename without path and extension for fallback */
|
|
||||||
const char* basename = strrchr(filename, '/');
|
|
||||||
if(!basename)
|
|
||||||
basename = filename;
|
|
||||||
else
|
|
||||||
basename++; /* Skip the '/' */
|
|
||||||
|
|
||||||
/* Remove file extension for display */
|
|
||||||
char displayName[64];
|
|
||||||
strncpy(displayName, basename, sizeof(displayName) - 1);
|
|
||||||
displayName[sizeof(displayName) - 1] = '\0';
|
|
||||||
char* dot = strrchr(displayName, '.');
|
|
||||||
if(dot) *dot = '\0';
|
|
||||||
|
|
||||||
/* Display song title */
|
|
||||||
if(metadata->title[0])
|
|
||||||
printf("%.47s\n", metadata->title);
|
|
||||||
else
|
|
||||||
printf("%.47s\n", displayName);
|
|
||||||
|
|
||||||
/* Display album */
|
|
||||||
if(metadata->album[0])
|
|
||||||
printf("%.47s\n", metadata->album);
|
|
||||||
else
|
|
||||||
printf("Unknown Album\n");
|
|
||||||
|
|
||||||
/* Display artist with album art indicator */
|
|
||||||
if(metadata->artist[0])
|
|
||||||
{
|
|
||||||
printf("%.45s", metadata->artist);
|
|
||||||
if(metadata->hasAlbumArt)
|
|
||||||
printf(" 🖼️");
|
|
||||||
printf("\n");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
printf("Unknown Artist");
|
|
||||||
if(metadata->hasAlbumArt)
|
|
||||||
printf(" 🖼️");
|
|
||||||
printf("\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -238,56 +194,135 @@ static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata)
|
|||||||
continue;
|
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;
|
if((uint8_t)text[0] == 0xFF && (uint8_t)text[1] == 0xFE)
|
||||||
|
|
||||||
/* 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)
|
|
||||||
{
|
{
|
||||||
dest = metadata->title;
|
text += 2;
|
||||||
maxLen = METADATA_TITLE_MAX - 1;
|
textLen -= 2;
|
||||||
}
|
}
|
||||||
else if(strncmp(frameId, "TPE1", 4) == 0)
|
else if((uint8_t)text[0] == 0xFE && (uint8_t)text[1] == 0xFF)
|
||||||
{
|
{
|
||||||
dest = metadata->artist;
|
text += 2;
|
||||||
maxLen = METADATA_ARTIST_MAX - 1;
|
textLen -= 2;
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 */
|
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)
|
if(frameSize > 10 && !metadata->hasAlbumArt)
|
||||||
{
|
{
|
||||||
metadata->hasAlbumArt = true;
|
uint8_t* frameData = malloc(frameSize);
|
||||||
metadata->albumArtSize = frameSize;
|
if(frameData && fread(frameData, 1, frameSize, fp) == frameSize)
|
||||||
/* Estimate dimensions - actual implementation would decode image */
|
{
|
||||||
metadata->albumArtWidth = 300; /* Common album art size */
|
/* ID3v2 APIC frame format:
|
||||||
metadata->albumArtHeight = 300;
|
* - 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
|
else
|
||||||
{
|
{
|
||||||
@@ -459,7 +494,72 @@ static int extractFlacMetadata(FILE* fp, struct metadata_t* metadata)
|
|||||||
blockType = blockHeader[0] & 0x7F;
|
blockType = blockHeader[0] & 0x7F;
|
||||||
blockSize = (blockHeader[1] << 16) | (blockHeader[2] << 8) | blockHeader[3];
|
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 */
|
/* FLAC uses Vorbis comments for metadata */
|
||||||
/* This is a simplified implementation */
|
/* This is a simplified implementation */
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "error.h"
|
#include "error.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
#include "flac.h"
|
#include "flac.h"
|
||||||
|
#include "m4a.h"
|
||||||
#include "mp3.h"
|
#include "mp3.h"
|
||||||
#include "opus.h"
|
#include "opus.h"
|
||||||
#include "playback.h"
|
#include "playback.h"
|
||||||
@@ -44,6 +45,16 @@ bool isPlaying(void)
|
|||||||
return !stop;
|
return !stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether playback is currently paused.
|
||||||
|
*/
|
||||||
|
bool isPaused(void)
|
||||||
|
{
|
||||||
|
if(stop)
|
||||||
|
return false;
|
||||||
|
return ndspChnIsPaused(CHANNEL);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should only be called from a new thread only, and have only one playback
|
* Should only be called from a new thread only, and have only one playback
|
||||||
* thread at time. This function has not been written for more than one
|
* thread at time. This function has not been written for more than one
|
||||||
@@ -88,15 +99,18 @@ void playFile(void* infoIn)
|
|||||||
setVorbis(&decoder);
|
setVorbis(&decoder);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FILE_TYPE_SID:
|
case FILE_TYPE_SID:
|
||||||
setSid(&decoder);
|
setSid(&decoder);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case FILE_TYPE_M4A:
|
||||||
|
case FILE_TYPE_AAC:
|
||||||
|
setM4a(&decoder);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
goto err;
|
goto err;
|
||||||
}
|
} if(ndspInit() < 0)
|
||||||
|
|
||||||
if(ndspInit() < 0)
|
|
||||||
{
|
{
|
||||||
errno = NDSP_INIT_FAIL;
|
errno = NDSP_INIT_FAIL;
|
||||||
goto err;
|
goto err;
|
||||||
|
|||||||
Reference in New Issue
Block a user