12 Commits

Author SHA1 Message Date
54a6dfddad Merge branch 'master' of git.sillyangel.dev:angel/mice-3ds
All checks were successful
Build (3DS) / build (push) Successful in 2m16s
2026-02-01 16:41:12 -06:00
e3683a12b7 feat: update build tasks, enhance metadata handling, and improve GUI text display 2026-02-01 16:39:05 -06:00
877e40b073 Update Makefile.linux
All checks were successful
Build (3DS) / build (push) Successful in 2m6s
Signed-off-by: angel <angel@sillyangel.dev>
2025-12-09 09:11:28 -06:00
88026628ea fix: url
Some checks failed
Build (3DS) / build (push) Failing after 57s
2025-12-07 16:49:58 -06:00
87669c74a5 feat: add unistore configuration and spritesheet assets for mice music player
Some checks failed
Build (3DS) / build (push) Has been cancelled
2025-12-07 16:49:20 -06:00
906734a14c fix: update copyright year to 2025 in main header files
All checks were successful
Build (3DS) / build (push) Successful in 2m37s
2025-12-07 15:58:52 -06:00
92647e2f1d workflow: fix the gosh damn thing
All checks were successful
Build (3DS) / build (push) Successful in 2m27s
2025-12-07 15:53:37 -06:00
36924ddfae feat: refactor album art handling, implement progress bar display, and update application version to dev63
Some checks failed
Build (3DS) / build (push) Failing after 2m6s
2025-12-07 15:39:55 -06:00
d09cf0739e feat: implement album art display functionality and update application version to dev50
Some checks failed
Build (3DS) / build (push) Failing after 1m59s
2025-12-06 23:31:45 -06:00
8be23ca4fc feat: add M4A/AAC support and update application version to dev43 2025-12-06 23:06:32 -06:00
5d9ce1fdb9 fix: update application version to dev37 and streamline playback logging
Some checks failed
Build (3DS) / build (push) Failing after 1m58s
2025-12-06 22:44:34 -06:00
e491cddfcb GUI 2025-12-06 22:42:06 -06:00
23 changed files with 1245 additions and 376 deletions

View File

@@ -51,9 +51,9 @@ jobs:
- name: Build
run: make -j"$(nproc)"
- name: Upload artefacts
uses: actions/upload-artifact@v4
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: mice-build
path: output
path: output/
if-no-files-found: warn

67
.vscode/tasks.json vendored
View File

@@ -1,6 +1,24 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Clean build artifacts",
"type": "shell",
"command": "rm",
"args": [
"-rf",
"${workspaceFolder}/build/*",
"${workspaceFolder}/output/3ds-arm/*"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "shared"
}
},
{
"label": "Build 3DS ROM",
"type": "shell",
@@ -12,10 +30,59 @@
"problemMatcher": [
"$gcc"
],
"presentation": {
"reveal": "always",
"panel": "shared"
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "Build (full)",
"type": "shell",
"command": "make",
"args": [],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"presentation": {
"reveal": "always",
"panel": "shared"
},
"group": "build"
},
{
"label": "Serve (dev)",
"type": "shell",
"command": "npm",
"args": [ "run", "dev" ],
"options": {
"cwd": "${workspaceFolder}/server"
},
"isBackground": true,
"presentation": {
"echo": true,
"reveal": "always",
"panel": "dedicated"
}
},
{
"label": "Serve (prod)",
"type": "shell",
"command": "npm",
"args": [ "start" ],
"options": {
"cwd": "${workspaceFolder}/server"
},
"presentation": {
"reveal": "always",
"panel": "shared"
}
}
]
}

View File

@@ -49,9 +49,9 @@ SOURCE_DIRS := source
EXTRA_OUTPUT_FILES :=
LIBRARY_DIRS := $(DEVKITPRO)/libctru $(DEVKITPRO)/portlibs/armv6k $(DEVKITPRO)/portlibs/3ds
LIBRARIES := sidplay mpg123 vorbisidec opusfile opus ogg ctru m
LIBRARIES := citro2d citro3d sidplay mpg123 vorbisidec opusfile opus ogg ctru m
BUILD_FLAGS := -Wall -Wextra -I$(DEVKITPRO)/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
RUN_FLAGS :=

View File

@@ -15,6 +15,8 @@ else
HOST_ARCH := x86_64
else ifeq ($(UNAME_M),$(filter $(UNAME_M),i386 i686))
HOST_ARCH := i686
else ifeq ($(UNAME_M),$(filter $(UNAME_M),aarch64 arm64))
HOST_ARCH := arm64
else
$(error "Unsupported host architecture.")
endif

View File

@@ -6,6 +6,7 @@ The latest 3DSX/CIA/3DS download can be found on the <a href="https://github.com
## Features
* 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.
* Plays music via headphones whilst system is closed.
* Ability to browse directories.

View File

@@ -6,7 +6,9 @@ enum file_types
FILE_TYPE_VORBIS,
FILE_TYPE_OPUS,
FILE_TYPE_MP3,
FILE_TYPE_SID
FILE_TYPE_SID,
FILE_TYPE_M4A,
FILE_TYPE_AAC
};
/**

126
include/gui.h Normal file
View File

@@ -0,0 +1,126 @@
#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 progress bar on top screen
*
* \param position Current position in seconds
* \param duration Total duration in seconds
*/
void guiDisplayProgressBar(float position, float duration);
/**
* Display current directory path on bottom screen
*
* \param path Current directory path
*/
void guiDisplayCurrentPath(const char* path);
#endif

21
include/m4a.h Normal file
View 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

View File

@@ -1,6 +1,6 @@
/**
* mice - 3DS Music Player
* Copyright (C) 2016 Mahyar Koshkouei
* Copyright (C) 2025 sillyangel
*
* This program comes with ABSOLUTELY NO WARRANTY and is free software. You are
* welcome to redistribute it under certain conditions; for details see the
@@ -13,7 +13,7 @@
#define mice_main_h
/* Application version */
#define MICE_VERSION "dev28"
#define MICE_VERSION "dev68"
/* Default folder */
#define DEFAULT_DIR "sdmc:/"

View File

@@ -13,13 +13,6 @@ struct metadata_t
char title[METADATA_TITLE_MAX];
char artist[METADATA_ARTIST_MAX];
char album[METADATA_ALBUM_MAX];
/* Album art */
uint8_t* albumArt;
size_t albumArtSize;
int albumArtWidth;
int albumArtHeight;
bool hasAlbumArt;
};
/**
@@ -46,11 +39,4 @@ void clearMetadata(struct metadata_t* metadata);
*/
void displayMetadata(struct metadata_t* metadata, const char* filename);
/**
* Display album art on top screen if available
*
* \param metadata Pointer to metadata structure containing album art
*/
void displayAlbumArt(struct metadata_t* metadata);
#endif

View File

@@ -80,6 +80,11 @@ void stopPlayback(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
* thread at time. This function has not been written for more than one

View File

@@ -1,4 +1,4 @@
## audio.wav
Ctrmus uses a modified version of [Rad Adventure by Scott Holmes](http://freemusicarchive.org/music/Scott_Holmes/~/Rad_Adventure) of which is licensed under a [Attribution-NonCommercial License](https://creativecommons.org/licenses/by-nc/4.0/).
Permission for use in ctrmus is granted.
Mice uses [Internal Monologues by Jon Shuemaker](https://freemusicarchive.org/music/jon-shuemaker/anoka-vol2/internal-monologues/) of which is licensed under a [Attribution-NonCommercial 4.0 International License.](https://creativecommons.org/licenses/by-nc/4.0/).
Permission for use in mice is granted.

Binary file not shown.

View File

@@ -5,6 +5,7 @@
#include "error.h"
#include "file.h"
#include "flac.h"
#include "m4a.h"
#include "mp3.h"
#include "opus.h"
#include "vorbis.h"
@@ -26,7 +27,9 @@ const char* fileToStr(enum file_types ft)
"VORBIS",
"OPUS",
"MP3",
"SID"
"SID",
"M4A",
"AAC"
};
return file_types_str[ft];
@@ -90,24 +93,37 @@ enum file_types getFileType(const char *file)
file_type = FILE_TYPE_SID;
break;
default:
/*
* 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;
default:
/* Check for M4A/AAC/ALAC (MP4 container with ftyp atom) */
if((fileSig == 0x70797466) || /* 'ftyp' at offset 4 */
(fileSig == 0x65657266)) /* 'free' at offset 4 (some M4A files) */
{
file_type = FILE_TYPE_M4A;
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);
return file_type;
}

418
source/gui.c Normal file
View File

@@ -0,0 +1,418 @@
#include <3ds.h>
#include <citro2d.h>
#include <citro3d.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.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
* @param screen Screen to draw on (GFX_TOP or GFX_BOTTOM)
* @param x X position
* @param y Y position
* @param text Text string to draw
* @param color Text color
* @param scale Text scale
*/
void guiDrawText(gfxScreen_t screen, float x, float y, const char* text, u32 color, float scale)
{
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
* @param metadata Pointer to metadata structure to display
* @param filename Filename to display if no title is available
*/
void guiDisplayMetadata(struct metadata_t* metadata, const char* filename)
{
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
* @param messages Array of log message strings
* @param count Number of messages in the array
* @param scroll Index of first visible message for scrolling
*/
void guiDisplayLog(const char** messages, int count, int scroll)
{
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
* @param files Array of file/folder names
* @param count Number of entries in the array
* @param selected Index of currently selected entry
* @param scroll Index of first visible entry for scrolling
*/
void guiDisplayFileList(const char** files, int count, int selected, int scroll)
{
if(!files || count <= 0 || !textBuf)
return;
C2D_SceneBegin(bottomTarget);
C2D_Text text;
float y = 18.0f; /* Start below path display */
float scale = 0.5f;
float lineHeight = 16.0f;
int maxLines = 13; /* One less line due to path at top */
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
* @param isPlaying Whether playback is active
* @param isPaused Whether playback is paused
* @param position Current playback position in seconds
* @param duration Total duration in seconds
*/
void guiDisplayPlaybackStatus(bool isPlaying, bool isPaused, float position, float duration)
{
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
* @param version Version string to display
*/
void guiDisplayVersion(const char* version)
{
if(!textBuf)
return;
C2D_SceneBegin(bottomTarget);
C2D_Text text;
C2D_TextBufClear(textBuf);
/* Display "mice - by sillyangel" at bottom center */
char credits[64];
if(version && version[0])
snprintf(credits, sizeof(credits), "mice %s - by sillyangel", version);
else
snprintf(credits, sizeof(credits), "mice - by sillyangel");
C2D_TextParse(&text, textBuf, credits);
C2D_TextOptimize(&text);
C2D_DrawText(&text, C2D_WithColor, 80.0f, 220.0f, 0.5f, 0.45f, 0.45f, GUI_COLOR_TEXT_DIM);
}
/**
* Display progress bar on the top screen
* @param position Current playback position in seconds
* @param duration Total duration in seconds
*/
void guiDisplayProgressBar(float position, float duration)
{
if(duration <= 0)
return;
C2D_SceneBegin(topTarget);
/* Progress bar at bottom of top screen */
float barY = 205.0f;
float barX = 10.0f;
float barWidth = 380.0f;
float barHeight = 6.0f;
/* Background bar */
C2D_DrawRectSolid(barX, barY, 0.5f, barWidth, barHeight, C2D_Color32(50, 50, 60, 255));
/* Progress fill */
float progress = position / duration;
if(progress > 1.0f) progress = 1.0f;
if(progress < 0.0f) progress = 0.0f;
float fillWidth = barWidth * progress;
if(fillWidth > 0)
{
C2D_DrawRectSolid(barX, barY, 0.5f, fillWidth, barHeight, GUI_COLOR_ACCENT);
}
}
/**
* Display current directory path on bottom screen
* @param path Current directory path
*/
void guiDisplayCurrentPath(const char* path)
{
if(!path || !textBuf)
return;
C2D_SceneBegin(bottomTarget);
C2D_Text text;
C2D_TextBufClear(textBuf);
/* Display path at top of bottom screen */
char pathBuf[64];
snprintf(pathBuf, sizeof(pathBuf), "%.55s", path);
C2D_TextParse(&text, textBuf, pathBuf);
C2D_TextOptimize(&text);
C2D_DrawText(&text, C2D_WithColor, 5.0f, 2.0f, 0.5f, 0.35f, 0.35f, GUI_COLOR_TEXT_DIM);
}

133
source/m4a.c Normal file
View 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;
}

View File

@@ -1,6 +1,6 @@
/**
* mice - 3DS Music Player
* Copyright (C) 2016 Mahyar Koshkouei
* Copyright (C) 2025 sillyangel
*
* This program comes with ABSOLUTELY NO WARRANTY and is free software. You are
* welcome to redistribute it under certain conditions; for details see the
@@ -11,6 +11,7 @@
#include <dirent.h>
#include <stdbool.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
@@ -21,23 +22,31 @@
#include "main.h"
#include "metadata.h"
#include "playback.h"
#include "gui.h"
volatile bool runThreads = true;
/**
* Prints the current key mappings to stdio.
*/
static void showControls(void)
{
printf("Button mappings:\n"
"Pause: L+R or L+Up\n"
"Previous/Next Song: ZL/ZR or L/R\n"
"A: Open File\n"
"B: Go up folder\n"
"Start: Exit\n"
"Browse: Up, Down, Left or Right\n");
/* 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.
@@ -56,19 +65,17 @@ void playbackWatchdog(void* infoIn)
if(*info->errInfo->error > 0)
{
continue;
consoleSelect(info->screen);
printf("Error %d: %s\n", *info->errInfo->error,
mice_strerror(*info->errInfo->error));
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)
{
continue;
/* Used to signify that playback has stopped.
* Not technically an error.
* Not technically an error. Don't spam logs.
*/
consoleSelect(info->screen);
puts("Stopped");
/* addLogMessage("Stopped"); */
}
}
@@ -115,7 +122,7 @@ static int changeFile(const char* ep_file, struct playbackInfo_t* playbackInfo)
//playbackInfo->file = strdup(ep_file);
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;
}
@@ -140,6 +147,125 @@ static int cmpstringp(const void *p1, const void *p2)
return strcasecmp(* (char * const *) p1, * (char * const *) p2);
}
/* Check filename extension against supported audio types. Case-insensitive. */
static bool isMusicFilename(const char *name)
{
if(name == NULL)
return false;
const char *ext = strrchr(name, '.');
if(ext == NULL || *(ext + 1) == '\0')
return false;
ext++; /* skip dot */
char lext[16];
size_t i = 0;
for(; i < sizeof(lext)-1 && ext[i]; i++)
lext[i] = tolower((unsigned char)ext[i]);
lext[i] = '\0';
const char *allowed[] = {"mp3","wav","flac","ogg","opus","sid","m4a","aac", NULL};
for(int j = 0; allowed[j] != NULL; j++)
if(strcmp(lext, allowed[j]) == 0)
return true;
return false;
}
/* Build a list of music filenames (basenames) in the given directory. Caller must free array and each string. */
static int buildMusicFileListInDir(const char *dirpath, char ***outFiles)
{
DIR *dp;
struct dirent *ep;
char **files = NULL;
int fileNum = 0;
if((dp = opendir(dirpath)) == NULL)
return -1;
while((ep = readdir(dp)) != NULL)
{
if(ep->d_type == DT_DIR)
continue;
if(!isMusicFilename(ep->d_name))
continue;
files = realloc(files, (fileNum + 1) * sizeof(char*));
files[fileNum] = strdup(ep->d_name);
fileNum++;
}
if(fileNum > 0)
qsort(files, fileNum, sizeof(char*), cmpstringp);
closedir(dp);
*outFiles = files;
return fileNum;
}
/* Play next file in the same folder as the currently-playing file. Returns 0 on success, -1 on failure or no-next. */
static int playNextFromPath(struct playbackInfo_t *playbackInfo)
{
if(playbackInfo == NULL || playbackInfo->file[0] == '\0')
return -1;
char fullcpy[PATH_MAX];
strncpy(fullcpy, playbackInfo->file, sizeof(fullcpy));
fullcpy[sizeof(fullcpy)-1] = '\0';
/* find last slash */
char *slash = strrchr(fullcpy, '/');
char dirpath[PATH_MAX];
char *basename = NULL;
if(slash == NULL)
{
/* No directory component; assume current dir */
if(getcwd(dirpath, sizeof(dirpath)) == NULL)
return -1;
basename = fullcpy;
}
else
{
size_t dirlen = slash - fullcpy;
if(dirlen >= sizeof(dirpath))
return -1;
memcpy(dirpath, fullcpy, dirlen);
dirpath[dirlen] = '\0';
basename = slash + 1;
}
char **files = NULL;
int count = buildMusicFileListInDir(dirpath, &files);
if(count <= 0)
return -1;
int index = -1;
for(int i = 0; i < count; i++)
{
if(strcmp(files[i], basename) == 0)
{
index = i;
break;
}
}
int result = -1;
if(index >= 0 && index + 1 < count)
{
char nextpath[PATH_MAX];
snprintf(nextpath, sizeof(nextpath), "%s/%s", dirpath, files[index+1]);
result = changeFile(nextpath, playbackInfo);
}
for(int i = 0; i < count; i++)
free(files[i]);
free(files);
return result;
}
/**
* Store the list of files and folders in current directory to an array.
*/
@@ -164,7 +290,7 @@ static int getDir(struct dirList_t* dirList)
free(dirList->currentDir);
if((dirList->currentDir = strdup(wd)) == NULL)
puts("Failure");
addLogMessage("Memory allocation failure");
if((dp = opendir(wd)) == NULL)
goto out;
@@ -177,7 +303,7 @@ static int getDir(struct dirList_t* dirList)
dirList->directories = realloc(dirList->directories, (dirNum + 1) * sizeof(char*));
if((dirList->directories[dirNum] = strdup(ep->d_name)) == NULL)
puts("Failure");
addLogMessage("Memory allocation failure");
dirNum++;
continue;
@@ -187,7 +313,7 @@ static int getDir(struct dirList_t* dirList)
dirList->files = realloc(dirList->files, (fileNum + 1) * sizeof(char*));
if((dirList->files[fileNum] = strdup(ep->d_name)) == NULL)
puts("Failure");
addLogMessage("Memory allocation failure");
fileNum++;
}
@@ -207,59 +333,53 @@ out:
}
/**
* List current directory.
*
* \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.
* Build file list for GUI display.
* Creates a combined list of directories and files for rendering.
*/
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;
int listed = 0;
printf("\033[0;0H");
printf("Dir: %.33s\n", dirList.currentDir);
static const char* combinedList[512];
static char entryBuffer[512][256];
int index = 0;
/* Add parent directory option */
if(from == 0)
{
printf("\33[2K%c../\n", select == 0 ? '>' : ' ');
listed++;
max--;
snprintf(entryBuffer[index], sizeof(entryBuffer[index]), "../");
combinedList[index] = entryBuffer[index];
index++;
}
while(dirList.fileNum + dirList.dirNum > fileNum)
/* Add all directories */
for(int i = 0; i < dirList.dirNum && index < 512; i++)
{
fileNum++;
if(fileNum <= from)
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;
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;
}
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 +410,12 @@ err:
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 fileNum = 0;
int from = 0;
Thread watchdogThread;
Thread watchdogThread __attribute__((unused));
Handle playbackFailEvent;
struct watchdogInfo watchdogInfoIn;
struct errInfo_t errInfo;
@@ -309,26 +428,17 @@ int main(int argc, char **argv)
bool keyLComboPressed = false;
bool keyRComboPressed = false;
gfxInitDefault();
consoleInit(GFX_TOP, &topScreenLog);
consoleInit(GFX_TOP, &topScreenInfo);
consoleInit(GFX_BOTTOM, &bottomScreen);
/* 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);
/* Initialize GUI system */
if(guiInit() != 0)
{
return -1;
}
svcCreateEvent(&playbackFailEvent, RESET_ONESHOT);
errInfo.error = &error;
errInfo.failEvent = &playbackFailEvent;
watchdogInfoIn.screen = &topScreenLog;
watchdogInfoIn.screen = NULL; /* No longer using console */
watchdogInfoIn.errInfo = &errInfo;
watchdogThread = threadCreate(playbackWatchdog,
&watchdogInfoIn, 4 * 1024, 0x20, -2, true);
@@ -346,13 +456,7 @@ int main(int argc, char **argv)
/* TODO: Not actually possible to get less than 0 */
if(getDir(&dirList) < 0)
{
puts("Unable to obtain directory information");
goto err;
}
if(listDir(from, MAX_LIST, 0, dirList) < 0)
{
err_print("Unable to list directory.");
addLogMessage("Unable to obtain directory information");
goto err;
}
@@ -371,25 +475,22 @@ int main(int argc, char **argv)
u32 kUp;
static u64 mill = 0;
gfxFlushBuffers();
gspWaitForVBlank();
gfxSwapBuffers();
/* Begin GUI frame */
guiBeginFrame();
guiClearTopScreen();
guiClearBottomScreen();
hidScanInput();
kDown = hidKeysDown();
kHeld = hidKeysHeld();
kUp = hidKeysUp();
consoleSelect(&bottomScreen);
/* Exit mice */
if(kDown & KEY_START)
break;
#ifdef DEBUG
consoleSelect(&topScreenLog);
printf("\rNum: %d, Max: %d, from: %d ", fileNum, fileMax, from);
consoleSelect(&bottomScreen);
/* Debug info logged if needed */
#endif
if(kDown)
mill = osGetTime();
@@ -402,11 +503,7 @@ int main(int argc, char **argv)
if(isPlaying() == false)
continue;
consoleSelect(&topScreenLog);
if(togglePlayback() == true)
puts("Paused");
else
puts("Playing");
togglePlayback();
keyLComboPressed = true;
// distinguish between L+R and L+Up
@@ -416,11 +513,9 @@ int main(int argc, char **argv)
continue;
}
/* Show controls */
/* Show controls - no longer needed with GUI */
if(kDown & KEY_LEFT)
{
consoleSelect(&topScreenLog);
showControls();
keyLComboPressed = true;
continue;
}
@@ -428,14 +523,10 @@ int main(int argc, char **argv)
// if R is pressed first
if ((kHeld & KEY_R) && (kDown & KEY_L))
{
if(isPlaying() == false)
if(isPlaying() == false)
continue;
consoleSelect(&topScreenLog);
if(togglePlayback() == true)
puts("Paused");
else
puts("Playing");
togglePlayback();
keyLComboPressed = true;
keyRComboPressed = true;
@@ -451,9 +542,6 @@ int main(int argc, char **argv)
// one line taken up by cwd, other by ../
if(fileMax - fileNum > MAX_LIST-2 && from != 0)
from--;
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
err_print("Unable to list directory.");
}
if((kDown & KEY_DOWN ||
@@ -465,9 +553,6 @@ int main(int argc, char **argv)
if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 &&
from < fileMax - MAX_LIST)
from++;
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
err_print("Unable to list directory.");
}
if((kDown & KEY_LEFT ||
@@ -485,12 +570,9 @@ int main(int argc, char **argv)
if(fileMax - fileNum > MAX_LIST-2 && from != 0)
{
from -= skip;
if(from < 0)
if(from < 0)
from = 0;
}
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
err_print("Unable to list directory.");
}
if((kDown & KEY_RIGHT ||
@@ -511,9 +593,6 @@ int main(int argc, char **argv)
if(from > fileMax - MAX_LIST)
from = fileMax - MAX_LIST;
}
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
err_print("Unable to list directory.");
}
/*
@@ -524,7 +603,6 @@ int main(int argc, char **argv)
((kDown & KEY_A) && (from == 0 && fileNum == 0)))
{
chdir("..");
consoleClear();
fileMax = getDir(&dirList);
fileNum = prevPosition[0];
@@ -537,9 +615,6 @@ int main(int argc, char **argv)
prevPosition[MAX_DIRECTORIES-1] = 0;
prevFrom[MAX_DIRECTORIES-1] = 0;
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
err_print("Unable to list directory.");
continue;
}
@@ -548,7 +623,6 @@ int main(int argc, char **argv)
if(dirList.dirNum >= fileNum)
{
chdir(dirList.directories[fileNum - 1]);
consoleClear();
fileMax = getDir(&dirList);
oldFileNum = fileNum;
@@ -556,36 +630,23 @@ int main(int argc, char **argv)
fileNum = 0;
from = 0;
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
{
err_print("Unable to list directory.");
}
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;
/* 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)
{
consoleSelect(&topScreenInfo);
consoleClear();
/* Extract and display metadata */
char fullPath[512];
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
extractMetadata(fullPath, &currentMetadata);
displayMetadata(&currentMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
consoleSelect(&topScreenLog);
//consoleClear();
guiDisplayMetadata(&currentMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
error = 0;
@@ -608,21 +669,15 @@ int main(int argc, char **argv)
if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 &&
from < fileMax - MAX_LIST)
from++;
consoleSelect(&topScreenInfo);
consoleClear();
/* Extract and display metadata */
char fullPath[512];
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
extractMetadata(fullPath, &currentMetadata);
displayMetadata(&currentMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
guiDisplayMetadata(&currentMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
consoleSelect(&topScreenLog);
//consoleClear();
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
error = 0;
consoleSelect(&bottomScreen);
if(listDir(from, MAX_LIST, fileNum, dirList) < 0) err_print("Unable to list directory.");
continue;
}
// ignore L release if key combo with L used
@@ -639,21 +694,15 @@ int main(int argc, char **argv)
fileNum -= 1;
if(fileMax - fileNum > MAX_LIST-2 && from != 0)
from--;
consoleSelect(&topScreenInfo);
consoleClear();
/* Extract and display metadata */
char fullPath[512];
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
extractMetadata(fullPath, &currentMetadata);
displayMetadata(&currentMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
guiDisplayMetadata(&currentMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
consoleSelect(&topScreenLog);
//consoleClear();
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
error = 0;
consoleSelect(&bottomScreen);
if(listDir(from, MAX_LIST, fileNum, dirList) < 0) err_print("Unable to list directory.");
continue;
}
@@ -665,78 +714,85 @@ int main(int argc, char **argv)
continue;
}
fileNum += 1;
consoleSelect(&topScreenInfo);
consoleClear();
/* Extract and display metadata */
char fullPath[512];
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
extractMetadata(fullPath, &currentMetadata);
displayMetadata(&currentMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
guiDisplayMetadata(&currentMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
consoleSelect(&topScreenLog);
//consoleClear();
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
error = 0;
consoleSelect(&bottomScreen);
if(listDir(from, MAX_LIST, fileNum, dirList) < 0) err_print("Unable to list directory.");
continue;
}
/* After 1000ms, update playback time. */
while(osGetTime() - mill > 1000)
/* 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])
{
consoleSelect(&topScreenLog);
/* Position cursor at bottom of log area for time display */
printf("\033[29;0H\033[K"); /* Move to line 29, clear line */
/* 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;
const char* currentFile = (fileNum > 0 && fileNum <= dirList.dirNum + dirList.fileNum) ?
(fileNum > dirList.dirNum ? dirList.files[fileNum - dirList.dirNum - 1] : "..") : "";
guiDisplayMetadata(&currentMetadata, 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:
puts("Exiting...");
addLogMessage("Exiting...");
runThreads = false;
clearMetadata(&currentMetadata);
svcSignalEvent(playbackFailEvent);
changeFile(NULL, &playbackInfo);
gfxExit();
/* Cleanup GUI */
guiExit();
/* Cleanup log messages */
for(int i = 0; i < logMessageCount; i++)
free(logMessages[i]);
return 0;
err:
puts("A fatal error occurred. Press START to exit.");
addLogMessage("A fatal error occurred. Press START to exit.");
while(true)
{

View File

@@ -7,6 +7,7 @@
#include "metadata.h"
#include "file.h"
#include "all.h"
#include "gui.h"
/* Internal helper functions */
static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata);
@@ -82,83 +83,16 @@ void clearMetadata(struct metadata_t* metadata)
memset(metadata->artist, 0, METADATA_ARTIST_MAX);
memset(metadata->album, 0, METADATA_ALBUM_MAX);
if(metadata->albumArt)
{
free(metadata->albumArt);
metadata->albumArt = NULL;
}
metadata->albumArtSize = 0;
metadata->albumArtWidth = 0;
metadata->albumArtHeight = 0;
metadata->hasAlbumArt = false;
}
/**
* 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)
{
if(!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");
}
}
/**
* Display album art on top screen if available
*/
void displayAlbumArt(struct metadata_t* metadata)
{
if(!metadata || !metadata->hasAlbumArt || !metadata->albumArt)
return;
/* For now, just indicate that album art is available */
/* Full implementation would require image decoding and display */
printf("🖼️ Album Art: %dx%d\n", metadata->albumArtWidth, metadata->albumArtHeight);
guiDisplayMetadata(metadata, filename);
}
/**
@@ -238,55 +172,91 @@ static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata)
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;
/* 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)
if((uint8_t)text[0] == 0xFF && (uint8_t)text[1] == 0xFE)
{
dest = metadata->title;
maxLen = METADATA_TITLE_MAX - 1;
text += 2;
textLen -= 2;
}
else if(strncmp(frameId, "TPE1", 4) == 0)
else if((uint8_t)text[0] == 0xFE && (uint8_t)text[1] == 0xFF)
{
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)
{
strncpy(dest, text, maxLen);
dest[maxLen] = 0;
trimWhitespace(dest);
text += 2;
textLen -= 2;
}
}
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 */
{
/* Basic album art detection - just store size info for now */
if(frameSize > 10 && !metadata->hasAlbumArt)
{
metadata->hasAlbumArt = true;
metadata->albumArtSize = frameSize;
/* Estimate dimensions - actual implementation would decode image */
metadata->albumArtWidth = 300; /* Common album art size */
metadata->albumArtHeight = 300;
}
/* Skip album art data */
fseek(fp, frameSize, SEEK_CUR);
}
else
@@ -459,7 +429,12 @@ static int extractFlacMetadata(FILE* fp, struct metadata_t* metadata)
blockType = blockHeader[0] & 0x7F;
blockSize = (blockHeader[1] << 16) | (blockHeader[2] << 8) | blockHeader[3];
if(blockType == 4) /* VORBIS_COMMENT */
if(blockType == 6) /* PICTURE */
{
/* Skip picture block */
fseek(fp, blockSize, SEEK_CUR);
}
else if(blockType == 4) /* VORBIS_COMMENT */
{
/* FLAC uses Vorbis comments for metadata */
/* This is a simplified implementation */

View File

@@ -7,6 +7,7 @@
#include "error.h"
#include "file.h"
#include "flac.h"
#include "m4a.h"
#include "mp3.h"
#include "opus.h"
#include "playback.h"
@@ -44,6 +45,16 @@ bool isPlaying(void)
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
* thread at time. This function has not been written for more than one
@@ -88,15 +99,18 @@ void playFile(void* infoIn)
setVorbis(&decoder);
break;
case FILE_TYPE_SID:
setSid(&decoder);
break;
case FILE_TYPE_SID:
setSid(&decoder);
break;
case FILE_TYPE_M4A:
case FILE_TYPE_AAC:
setM4a(&decoder);
break;
default:
goto err;
}
if(ndspInit() < 0)
default:
goto err;
} if(ndspInit() < 0)
{
errno = NDSP_INIT_FAIL;
goto err;

44
unistore/mice.unistore Normal file
View File

@@ -0,0 +1,44 @@
{
"storeInfo": {
"title": "mice unistore",
"author": "sillyangel",
"description": "A music player for the Nintendo 3DS",
"url": "https://git.sillyangel.dev/angel/mice-3ds/raw/branch/master/unistore/mice.unistore",
"file": "mice.unistore",
"sheetURL": "https://git.sillyangel.dev/angel/mice-3ds/raw/branch/master/unistore/spritesheet/sheet.t3x",
"sheet": "sheet.t3x",
"version": 4,
"revision": 0
},
"storeContent": [
{
"info": {
"title": "mice",
"author": "sillyangel",
"description": "A music player for the Nintendo 3DS. Plays PCM WAV, AIFF, FLAC, Opus, Vorbis and MP3 files.",
"category": ["utility", "music"],
"console": ["3DS"],
"icon_index": 0,
"sheet_index": 0,
"last_updated": "2025-12-07 at 00:00 (UTC)",
"license": "gpl-3.0",
"version": "dev63"
}
"mice.cia": [
{
"type": "downloadFile",
"file": "hhttps://git.sillyangel.dev/angel/mice-3ds/releases/download/latest/mice.cia",
"output": "sdmc:/mice.cia"
},
{
"type": "installCia",
"file": "/mice.cia"
},
{
"type": "deleteFile",
"file": "sdmc:/mice.cia"
}
],
}
]
}

BIN
unistore/spritesheet/0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,3 @@
--atlas -f rgba -z auto
0.png

Binary file not shown.