11 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
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
ab9b766be3 fix: adjust console window sizes for better display
Some checks failed
Build (3DS) / build (push) Failing after 2m2s
2025-12-06 21:14:57 -06:00
2ded2a5a00 Refactor code structure for improved readability and maintainability
Some checks failed
Build (3DS) / build (push) Failing after 2m10s
2025-12-06 21:08:15 -06:00
e833b79458 new: added some server
Some checks failed
Build (3DS) / build (push) Failing after 2m14s
2025-12-07 01:21:43 +00:00
53322f8767 workflow: Install Node.js and update artifact name in 3DS build 2025-12-07 01:18:27 +00:00
22 changed files with 3952 additions and 376 deletions

View File

@@ -25,6 +25,11 @@ jobs:
PATH: /opt/devkitpro/tools/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PATH: /opt/devkitpro/tools/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
steps: steps:
- name: Install Node.js (required for GitHub Actions)
run: |
apt-get update
apt-get install -y nodejs npm
- name: Checkout (with submodules) - name: Checkout (with submodules)
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -46,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: ctrmus-build name: mice-build
path: output path: output/
if-no-files-found: warn if-no-files-found: warn

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
build build
output output
server node_modules
# Object files # Object files
*.o *.o

View File

@@ -1,3 +1,4 @@
{ {
"C_Cpp.errorSquiggles": "disabled" "C_Cpp.errorSquiggles": "disabled",
"cmake.sourceDirectory": "/home/angel/Documents/mice-3ds/include/dr_libs"
} }

View File

@@ -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 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 :=

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

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 * 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 "dev20" #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

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

57
server/README.md Normal file
View File

@@ -0,0 +1,57 @@
# mice Download Server
Simple Node.js server to host mice 3DS homebrew files for easy download via QR code.
## Quick Start
1. **Install dependencies:**
```bash
cd server
npm install
```
2. **Build mice first:****
```bash
cd ..
make
```
3. **Start the server:**
```bash
cd server
npm start
```
4. **Access the download page:**
- Open browser to `http://localhost:3000`
- Use QR codes to download on 3DS browser
- Or find your local IP and access from any device on the network
## Features
- 📱 QR codes for easy 3DS browser access
- 📦 Automatic detection of build files (.3dsx, .cia, .3ds, .elf)
- 🎮 Installation instructions included
- 📊 File size information
- 🌐 Network accessible from any device
## Usage on 3DS
1. Connect your 3DS to the same WiFi network as your computer
2. Open the Internet Browser on your 3DS
3. Scan the QR code or manually enter the URL
4. Download the .3dsx or .cia file
5. Install according to the instructions on the page
## Development
```bash
npm run dev # Uses nodemon for auto-restart
```
## File Types
- **.3dsx** - Homebrew Launcher format (copy to `/3ds/` folder)
- **.cia** - Installable format (use FBI or similar installer)
- **.3ds** - 3DS ROM format
- **.elf** - Debug/development format

1465
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
server/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "mice-server",
"version": "1.0.0",
"description": "Simple file server for mice 3DS homebrew downloads",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"qrcode": "^1.5.3"
},
"devDependencies": {
"nodemon": "^3.0.2"
},
"keywords": ["3ds", "homebrew", "file-server"],
"author": "3DS Dev",
"license": "MIT"
}

1072
server/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

346
server/server.js Normal file
View File

@@ -0,0 +1,346 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
const QRCode = require('qrcode');
const app = express();
const PORT = process.env.PORT || 4012;
// Function to read current version from main.h
function getCurrentVersion() {
try {
const mainHPath = path.join(__dirname, '../include/main.h');
const content = fs.readFileSync(mainHPath, 'utf8');
const versionMatch = content.match(/#define\s+MICE_VERSION\s+"([^"]+)"/);
return versionMatch ? versionMatch[1] : 'unknown';
} catch (error) {
console.log('Could not read version:', error.message);
return 'unknown';
}
}
// Function to get build timestamp
function getBuildTimestamp() {
try {
const outputDir = path.join(__dirname, '../output');
if (fs.existsSync(outputDir)) {
const stats = fs.statSync(outputDir);
return stats.mtime.toLocaleString();
}
} catch (error) {
console.log('Could not read build timestamp:', error.message);
}
return 'unknown';
}
// Serve static files from the output directory
app.use('/downloads', express.static(path.join(__dirname, '../output')));
// Basic styling for the web interface with dark mode support
const CSS = `
<style>
:root {
--bg-color: #f5f5f5;
--container-bg: white;
--text-color: #333;
--border-color: #ddd;
--section-bg: #fafafa;
--accent-color: #0066cc;
--button-hover: #0052a3;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a;
--container-bg: #2d2d2d;
--text-color: #e0e0e0;
--border-color: #555;
--section-bg: #3a3a3a;
--accent-color: #4da6ff;
--button-hover: #3d8bff;
}
}
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
}
.container {
background: var(--container-bg);
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 20px rgba(0,0,0,0.3);
transition: background-color 0.3s;
}
h1 {
color: var(--text-color);
text-align: center;
border-bottom: 3px solid var(--accent-color);
padding-bottom: 10px;
}
.version-info {
text-align: center;
margin: 10px 0 20px 0;
padding: 10px;
background: var(--section-bg);
border-radius: 5px;
font-size: 14px;
color: var(--text-color);
}
.download-section {
margin: 30px 0;
padding: 20px;
border: 2px solid var(--border-color);
border-radius: 8px;
background-color: var(--section-bg);
transition: background-color 0.3s;
}
.download-link {
display: inline-block;
padding: 12px 24px;
background-color: var(--accent-color);
color: white;
text-decoration: none;
border-radius: 5px;
margin: 10px 10px 10px 0;
font-weight: bold;
transition: background-color 0.3s;
}
.download-link:hover {
background-color: var(--button-hover);
}
.download-link:hover {
background-color: #0052a3;
}
.qr-code {
text-align: center;
margin: 20px 0;
}
.file-info {
background-color: #e8f4fd;
padding: 15px;
border-left: 4px solid #0066cc;
margin: 15px 0;
}
.installation-guide {
background-color: #fff8dc;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
@media (prefers-color-scheme: dark) {
.file-info {
background-color: #1a2940;
border-left-color: #2d5aa0;
}
.installation-guide {
background-color: #2a3b5c;
}
}
</style>`;
// Main page
app.get('/', async (req, res) => {
const outputDir = path.join(__dirname, '../output');
// Use the client's actual request URL
const protocol = req.get('x-forwarded-proto') || req.protocol || 'http';
const host = req.get('x-forwarded-host') || req.get('host');
const baseUrl = `${protocol}://${host}`;
// Check if build files exist
const files = [];
const fileExtensions = ['.3dsx', '.cia', '.3ds', '.elf'];
try {
const items = fs.readdirSync(outputDir, { recursive: true });
for (const item of items) {
const itemPath = path.join(outputDir, item);
const stats = fs.statSync(itemPath);
if (stats.isFile()) {
const ext = path.extname(item).toLowerCase();
if (fileExtensions.includes(ext)) {
const filePath = `/downloads/${item}`;
const fileUrl = `${baseUrl}${filePath}`;
files.push({
name: item,
path: filePath,
url: fileUrl,
size: (stats.size / 1024).toFixed(1), // KB
type: ext.substring(1).toUpperCase()
});
}
}
}
} catch (err) {
console.error('Error reading output directory:', err);
}
let html = `
<!DOCTYPE html>
<html>
<head>
<title>mice - 3DS Music Player Downloads</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="refresh" content="30">
${CSS}
<script>
// Auto-refresh every 30 seconds to check for new builds
setInterval(() => {
fetch('/api/version')
.then(response => response.json())
.then(data => {
const versionElement = document.getElementById('current-version');
const buildElement = document.getElementById('build-time');
if (versionElement) versionElement.textContent = data.version;
if (buildElement) buildElement.textContent = data.buildTime;
})
.catch(console.error);
}, 30000);
</script>
</head>
<body>
<div class="container">
<h1>🎵 mice - 3DS Music Player 🎵</h1>
<div class="version-info">
<strong>Current Version:</strong> <span id="current-version">${getCurrentVersion()}</span><br>
<strong>Last Build:</strong> <span id="build-time">${getBuildTimestamp()}</span>
</div>
<div class="installation-guide">
<h3>📱 Installation Instructions:</h3>
<p><strong>For .3dsx files (Homebrew Launcher):</strong></p>
<ul>
<li>Copy the .3dsx file to <code>/3ds/</code> folder on your SD card</li>
<li>Launch via Homebrew Launcher</li>
</ul>
<p><strong>For .cia files (installed to HOME menu):</strong></p>
<ul>
<li>Install using FBI, DevMenu, or similar CIA installer</li>
<li>Appears on HOME menu after installation</li>
</ul>
</div>
`;
if (files.length === 0) {
html += `
<div class="file-info">
<h3>⚠️ No build files found</h3>
<p>Please run <code>make</code> in the mice directory to build the project first.</p>
</div>
`;
} else {
html += `<h2>📦 Available Downloads:</h2>`;
for (const file of files) {
// Generate QR code for each file
let qrCodeDataUrl = '';
try {
qrCodeDataUrl = await QRCode.toDataURL(file.url);
} catch (err) {
console.error('Error generating QR code:', err);
}
html += `
<div class="download-section">
<h3>🎮 ${file.name}</h3>
<div class="file-info">
<strong>Type:</strong> ${file.type} &nbsp;&nbsp;
<strong>Size:</strong> ${file.size} KB
</div>
<a href="${file.path}" class="download-link" download>
📥 Download ${file.type}
</a>
${qrCodeDataUrl ? `
<div class="qr-code">
<p><strong>📱 Scan QR Code for 3DS Browser:</strong></p>
<img src="${qrCodeDataUrl}" alt="QR Code for ${file.name}" style="border: 2px solid #ddd; padding: 10px; background: white;">
<br>
<small style="color: #666; word-break: break-all;">${file.url}</small>
</div>
` : ''}
</div>
`;
}
}
html += `
<div class="file-info">
<h3>🎵 Features:</h3>
<ul>
<li>Support for MP3, FLAC, OGG Vorbis, Opus, WAV, and SID formats</li>
<li>Metadata display (artist, album, title) on top screen</li>
<li>File browser with folder navigation</li>
<li>Playback controls and sleep mode support</li>
</ul>
</div>
<div class="installation-guide">
<h3>🔗 Access Information:</h3>
<p>Server running at: <strong>${baseUrl}</strong></p>
${baseUrl.includes('github.dev') ?
'<p>✅ GitHub Codespaces detected - URL is ready for 3DS browser!</p>' :
'<p>💡 For 3DS access, use your local network IP address</p>'
}
</div>
</div>
</body>
</html>
`;
res.send(html);
});
// API endpoint for version info
app.get('/api/version', (req, res) => {
res.json({
version: getCurrentVersion(),
buildTime: getBuildTimestamp(),
timestamp: new Date().toISOString()
});
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Start server
app.listen(PORT, '0.0.0.0', () => {
console.log(`🎵 mice Download Server`);
// Detect if we're in GitHub Codespaces
const codespace = process.env.CODESPACE_NAME;
if (codespace) {
const codespacesUrl = `https://${codespace}-${PORT}.app.github.dev/`;
console.log(`🌐 GitHub Codespaces URL: ${codespacesUrl}`);
console.log(`📱 Use this URL for 3DS browser access!`);
} else {
console.log(`🌐 Server running at: http://localhost:${PORT}`);
console.log(`📱 Access from 3DS browser using your local IP`);
}
console.log(`📦 Serving files from: ${path.join(__dirname, '../output')}`);
console.log(`\n🔗 To find your local IP address:`);
console.log(` - Windows: ipconfig`);
console.log(` - Mac/Linux: ifconfig or ip addr show`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('🛑 Server shutting down...');
process.exit(0);
});
module.exports = app;

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

393
source/gui.c Normal file
View File

@@ -0,0 +1,393 @@
#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
*/
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 = 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
*/
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);
}
/**
/**
* 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
@@ -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;
} }
return listed; /* Add all files */
for(int i = 0; i < dirList.fileNum && index < 512; i++)
{
snprintf(entryBuffer[index], sizeof(entryBuffer[index]), "%s", dirList.files[i]);
combinedList[index] = entryBuffer[index];
index++;
}
*outList = combinedList;
*outCount = index;
}
/**
* Dummy function kept for compatibility (no longer used with GUI)
*/
static int listDir(int from __attribute__((unused)),
int max __attribute__((unused)),
int select __attribute__((unused)),
struct dirList_t dirList __attribute__((unused)))
{
/* This function is no longer used with GUI rendering */
return 0;
} }
/** /**
@@ -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, 3, 50, 28);
consoleSetWindow(&topScreenInfo, 1, 1, 50, 2);
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, &currentMetadata); 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); 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, &currentMetadata); 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); 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, &currentMetadata); 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); 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,85 @@ 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, &currentMetadata); 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); 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(&currentMetadata, currentFile);
/* 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 (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: out:
puts("Exiting..."); addLogMessage("Exiting...");
runThreads = false; runThreads = false;
clearMetadata(&currentMetadata); clearMetadata(&currentMetadata);
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)
{ {

View File

@@ -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);
@@ -82,83 +83,16 @@ 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;
} }
/** /**
* 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");
}
}
/**
* 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);
} }
/** /**
@@ -238,55 +172,91 @@ 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 */ /* 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
@@ -459,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"
@@ -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;
default: case FILE_TYPE_M4A:
goto err; case FILE_TYPE_AAC:
} setM4a(&decoder);
break;
if(ndspInit() < 0) default:
goto err;
} if(ndspInit() < 0)
{ {
errno = NDSP_INIT_FAIL; errno = NDSP_INIT_FAIL;
goto err; goto err;