5 Commits

Author SHA1 Message Date
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
13 changed files with 380 additions and 125 deletions

View File

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

View File

@@ -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.

View File

@@ -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
}; };
/** /**

View File

@@ -108,4 +108,19 @@ void guiDisplayVersion(const char* version);
*/ */
void guiDrawText(gfxScreen_t screen, float x, float y, const char* text, u32 color, float scale); 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 #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 * 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 * This program comes with ABSOLUTELY NO WARRANTY and is free software. You are
* welcome to redistribute it under certain conditions; for details see the * welcome to redistribute it under certain conditions; for details see the
@@ -13,7 +13,7 @@
#define mice_main_h #define mice_main_h
/* Application version */ /* Application version */
#define MICE_VERSION "dev37" #define MICE_VERSION "dev63"
/* Default folder */ /* Default folder */
#define DEFAULT_DIR "sdmc:/" #define DEFAULT_DIR "sdmc:/"

View File

@@ -14,12 +14,7 @@ struct metadata_t
char artist[METADATA_ARTIST_MAX]; char artist[METADATA_ARTIST_MAX];
char album[METADATA_ALBUM_MAX]; char album[METADATA_ALBUM_MAX];
/* Album art */
uint8_t* albumArt;
size_t albumArtSize;
int albumArtWidth;
int albumArtHeight;
bool hasAlbumArt;
}; };
/** /**
@@ -46,11 +41,4 @@ void clearMetadata(struct metadata_t* metadata);
*/ */
void displayMetadata(struct metadata_t* metadata, const char* filename); 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 #endif

View File

@@ -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];
@@ -91,6 +94,21 @@ enum file_types getFileType(const char *file)
break; break;
default: 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;
}
/* /*
* MP3 without ID3 tag, ID3v1 tag is at the end of file, or MP3 * MP3 without ID3 tag, ID3v1 tag is at the end of file, or MP3
* with ID3 tag at the beginning of the file. * with ID3 tag at the beginning of the file.
@@ -105,9 +123,7 @@ enum file_types getFileType(const char *file)
//printf("Unknown magic number: %#010x\n.", fileSig); //printf("Unknown magic number: %#010x\n.", fileSig);
errno = FILE_NOT_SUPPORTED; errno = FILE_NOT_SUPPORTED;
break; break;
} }err:
err:
fclose(ftest); fclose(ftest);
return file_type; return file_type;
} }

View File

@@ -3,6 +3,7 @@
#include <citro3d.h> #include <citro3d.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include "gui.h" #include "gui.h"
#include "metadata.h" #include "metadata.h"
@@ -180,14 +181,6 @@ void guiDisplayMetadata(struct metadata_t* metadata, const char* filename)
C2D_TextOptimize(&text); C2D_TextOptimize(&text);
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale * 0.8f, scale * 0.8f, GUI_COLOR_TEXT_DIM); C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale * 0.8f, scale * 0.8f, GUI_COLOR_TEXT_DIM);
} }
/* Draw album art indicator if available */
if(metadata->hasAlbumArt)
{
C2D_TextParse(&text, textBuf, "[Art]");
C2D_TextOptimize(&text);
C2D_DrawText(&text, C2D_WithColor, 350.0f, 10.0f, 0.5f, 0.4f, 0.4f, GUI_COLOR_ACCENT);
}
} }
/** /**
@@ -231,10 +224,10 @@ void guiDisplayFileList(const char** files, int count, int selected, int scroll)
C2D_SceneBegin(bottomTarget); C2D_SceneBegin(bottomTarget);
C2D_Text text; C2D_Text text;
float y = 10.0f; float y = 18.0f; /* Start below path display */
float scale = 0.5f; float scale = 0.5f;
float lineHeight = 16.0f; float lineHeight = 16.0f;
int maxLines = 14; int maxLines = 13; /* One less line due to path at top */
C2D_TextBufClear(textBuf); C2D_TextBufClear(textBuf);
@@ -343,3 +336,58 @@ void guiDisplayVersion(const char* version)
C2D_TextOptimize(&text); C2D_TextOptimize(&text);
C2D_DrawText(&text, C2D_WithColor, 80.0f, 220.0f, 0.5f, 0.45f, 0.45f, GUI_COLOR_TEXT_DIM); C2D_DrawText(&text, C2D_WithColor, 80.0f, 220.0f, 0.5f, 0.45f, 0.45f, GUI_COLOR_TEXT_DIM);
} }
/**
/**
* Display progress bar on top screen
*/
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
*/
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 * 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 * This program comes with ABSOLUTELY NO WARRANTY and is free software. You are
* welcome to redistribute it under certain conditions; for details see the * welcome to redistribute it under certain conditions; for details see the
@@ -619,12 +619,19 @@ int main(int argc __attribute__((unused)), char **argv __attribute__((unused)))
guiDisplayMetadata(&currentMetadata, currentFile); guiDisplayMetadata(&currentMetadata, currentFile);
} }
/* Calculate scroll position to keep selection visible (14 lines visible) */ /* Calculate scroll position to keep selection visible (13 lines visible with path) */
int scroll = from; int scroll = from;
if(fileNum < scroll) if(fileNum < scroll)
scroll = fileNum; scroll = fileNum;
else if(fileNum >= scroll + 14) else if(fileNum >= scroll + 13)
scroll = fileNum - 13; scroll = fileNum - 12;
/* Display current directory path */
char currentPath[256];
if(getcwd(currentPath, sizeof(currentPath)))
{
guiDisplayCurrentPath(currentPath);
}
/* Display file list on bottom screen */ /* Display file list on bottom screen */
guiDisplayFileList(fileList, fileListCount, fileNum, scroll); guiDisplayFileList(fileList, fileListCount, fileNum, scroll);
@@ -632,11 +639,12 @@ int main(int argc __attribute__((unused)), char **argv __attribute__((unused)))
/* Display logs on top screen */ /* Display logs on top screen */
guiDisplayLog((const char**)logMessages, logMessageCount, logScroll); guiDisplayLog((const char**)logMessages, logMessageCount, logScroll);
/* Display playback status */ /* Display playback status and progress bar */
if(playbackInfo.samples_per_second > 0) if(playbackInfo.samples_per_second > 0)
{ {
float position = (float)playbackInfo.samples_played / playbackInfo.samples_per_second; float position = (float)playbackInfo.samples_played / playbackInfo.samples_per_second;
float duration = (float)playbackInfo.samples_total / playbackInfo.samples_per_second; float duration = (float)playbackInfo.samples_total / playbackInfo.samples_per_second;
guiDisplayProgressBar(position, duration);
guiDisplayPlaybackStatus(isPlaying(), isPaused(), position, duration); guiDisplayPlaybackStatus(isPlaying(), isPaused(), position, duration);
} }

View File

@@ -83,16 +83,7 @@ void clearMetadata(struct metadata_t* metadata)
memset(metadata->artist, 0, METADATA_ARTIST_MAX); memset(metadata->artist, 0, METADATA_ARTIST_MAX);
memset(metadata->album, 0, METADATA_ALBUM_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;
} }
/** /**
@@ -104,19 +95,6 @@ void displayMetadata(struct metadata_t* metadata, const char* filename)
guiDisplayMetadata(metadata, filename); guiDisplayMetadata(metadata, filename);
} }
/**
* 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);
}
/** /**
* Extract ID3v2 metadata from MP3 file * Extract ID3v2 metadata from MP3 file
*/ */
@@ -198,10 +176,31 @@ static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata)
{ {
frameData[frameSize] = 0; frameData[frameSize] = 0;
/* Skip text encoding byte */ /* 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; char* text = frameData + 1;
int textLen = frameSize - 1; int textLen = frameSize - 1;
/* Skip UTF-16 BOM if present */
if(encoding == 0x01 && textLen >= 2)
{
if((uint8_t)text[0] == 0xFF && (uint8_t)text[1] == 0xFE)
{
text += 2;
textLen -= 2;
}
else if((uint8_t)text[0] == 0xFE && (uint8_t)text[1] == 0xFF)
{
text += 2;
textLen -= 2;
}
}
/* Copy to appropriate field */ /* Copy to appropriate field */
char* dest = NULL; char* dest = NULL;
size_t maxLen = 0; size_t maxLen = 0;
@@ -224,25 +223,40 @@ static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata)
if(dest) if(dest)
{ {
strncpy(dest, text, maxLen); /* Handle different encodings */
dest[maxLen] = 0; 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); trimWhitespace(dest);
} }
} } free(frameData);
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 */ /* Skip album art data */
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;
}
fseek(fp, frameSize, SEEK_CUR); fseek(fp, frameSize, SEEK_CUR);
} }
else else
@@ -415,7 +429,12 @@ 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 */
{
/* Skip picture block */
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 */

View File

@@ -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"
@@ -102,11 +103,14 @@ void playFile(void* infoIn)
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;