Compare commits
26 Commits
8f5a55c27b
...
dev63
| Author | SHA1 | Date | |
|---|---|---|---|
|
906734a14c
|
|||
|
92647e2f1d
|
|||
|
36924ddfae
|
|||
|
d09cf0739e
|
|||
|
8be23ca4fc
|
|||
|
5d9ce1fdb9
|
|||
|
e491cddfcb
|
|||
|
ab9b766be3
|
|||
|
2ded2a5a00
|
|||
|
e833b79458
|
|||
|
53322f8767
|
|||
| 285351e982 | |||
|
|
ecac9e6064 | ||
|
|
8b028eb41b | ||
|
|
909effaaef | ||
|
|
227000e7e3 | ||
|
|
f8c2ede98b | ||
|
|
81b85b6c53 | ||
|
|
02007c48eb | ||
|
|
5820223214 | ||
|
|
4401238766 | ||
|
|
47106bd059 | ||
|
|
bd283f7f5f | ||
|
|
28e3275bdc | ||
|
|
576d320fb7 | ||
|
|
fbf9db3e45 |
59
.github/workflows/3ds.yml
vendored
Normal file
59
.github/workflows/3ds.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: Build (3DS)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, actions ]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
# Build inside the official devkitPro image (has dkp-pacman & toolchains preconfigured)
|
||||
container:
|
||||
image: devkitpro/devkitarm:latest
|
||||
options: --user root
|
||||
|
||||
env:
|
||||
DEVKITPRO: /opt/devkitpro
|
||||
DEVKITARM: /opt/devkitpro/devkitARM
|
||||
PATH: /opt/devkitpro/tools/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
steps:
|
||||
- name: Install Node.js (required for GitHub Actions)
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y nodejs npm
|
||||
|
||||
- name: Checkout (with submodules)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install makerom & bannertool (Linux x64)
|
||||
run: |
|
||||
MAKEROM_VER=makerom-v0.18.4
|
||||
BANNERP_URL="https://github.com/Epicpkmn11/bannertool/releases/download/v1.2.2/bannertool.zip"
|
||||
set -euxo pipefail
|
||||
mkdir -p /opt/devkitpro/tools/bin
|
||||
curl -L "https://github.com/3DSGuy/Project_CTR/releases/download/${MAKEROM_VER}/${MAKEROM_VER}-ubuntu_x86_64.zip" -o /tmp/makerom.zip
|
||||
unzip -j /tmp/makerom.zip -d /opt/devkitpro/tools/bin
|
||||
chmod +x /opt/devkitpro/tools/bin/makerom
|
||||
curl -L "$BANNERP_URL" -o /tmp/bannertool.zip
|
||||
unzip -p /tmp/bannertool.zip 'linux-x86_64/bannertool' > /opt/devkitpro/tools/bin/bannertool
|
||||
chmod +x /opt/devkitpro/tools/bin/bannertool
|
||||
|
||||
- name: Build
|
||||
run: make -j"$(nproc)"
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mice-build
|
||||
path: output/
|
||||
if-no-files-found: warn
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
build
|
||||
output
|
||||
node_modules
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"C_Cpp.errorSquiggles": "disabled"
|
||||
"C_Cpp.errorSquiggles": "disabled",
|
||||
"cmake.sourceDirectory": "/home/angel/Documents/mice-3ds/include/dr_libs"
|
||||
}
|
||||
17
Makefile
17
Makefile
@@ -39,7 +39,7 @@ endif
|
||||
|
||||
# COMMON CONFIGURATION #
|
||||
|
||||
NAME := ctrmus
|
||||
NAME := mice
|
||||
|
||||
BUILD_DIR := build
|
||||
OUTPUT_DIR := output
|
||||
@@ -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 :=
|
||||
|
||||
@@ -64,10 +64,10 @@ VERSION_MICRO := $(word 3, $(VERSION_PARTS))
|
||||
# 3DS CONFIGURATION #
|
||||
|
||||
TITLE := $(NAME)
|
||||
DESCRIPTION := 3DS Music Player
|
||||
AUTHOR := Deltabeard
|
||||
PRODUCT_CODE := CTR-P-CMUS
|
||||
UNIQUE_ID := 0xFF3CC
|
||||
DESCRIPTION := local files music player
|
||||
AUTHOR := sillyangel
|
||||
PRODUCT_CODE := CTR-P-MICE
|
||||
UNIQUE_ID := 0xFB1CE
|
||||
|
||||
SYSTEM_MODE := 64MB
|
||||
SYSTEM_MODE_EXT := Legacy
|
||||
@@ -81,4 +81,7 @@ ICON := meta/icon.png
|
||||
|
||||
# INTERNAL #
|
||||
|
||||
# Auto-increment version before build
|
||||
$(shell ./increment_version.sh >/dev/null 2>&1)
|
||||
|
||||
include buildtools/make_base
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<img align="right" alt="The ctrmus icon" src="meta/icon.png">
|
||||
<img align="right" alt="The mice icon" src="meta/icon.png">
|
||||
|
||||
# ctrmus: a music player for the Nintendo 3DS
|
||||
# mice: a music player for the Nintendo 3DS
|
||||
|
||||
The latest 3DSX/CIA/3DS download can be found on the <a href="https://github.com/deltabeard/ctrmus/releases">releases</a> page, or by scanning <a href="https://zxing.org/w/chart?cht=qr&chs=230x230&chld=L&choe=UTF-8&chl=https%3A%2F%2Fgithub.com%2Fdeltabeard%2Fctrmus%2Freleases%2Fdownload%2F0.4.12%2Fctrmus.cia">this QR code</a>.
|
||||
The latest 3DSX/CIA/3DS download can be found on the <a href="https://github.com/deltabeard/ctrmus/releases">releases</a> page, or by scanning <a href="https://zxing.org/w/chart?cht=qr&chs=350x350&chld=M&choe=UTF-8&chl=https%3A%2F%2Fgithub.com%2Fdeltabeard%2Fctrmus%2Freleases%2Fdownload%2Fv0.5.3%2Fctrmus.cia">this QR code</a>.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -27,10 +27,10 @@ struct errInfo_t
|
||||
|
||||
/**
|
||||
* Return string describing error number. Extends strerror to include some
|
||||
* custom errors used in ctrmus.
|
||||
* custom errors used in mice.
|
||||
*
|
||||
* \param err Error number.
|
||||
*/
|
||||
char* ctrmus_strerror(int err);
|
||||
char* mice_strerror(int err);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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
126
include/gui.h
Normal 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
21
include/m4a.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef M4A_H
|
||||
#define M4A_H
|
||||
|
||||
#include "playback.h"
|
||||
|
||||
/**
|
||||
* Set decoder parameters for M4A/AAC.
|
||||
*
|
||||
* \param decoder Structure to store parameters.
|
||||
*/
|
||||
void setM4a(struct decoder_fn* decoder);
|
||||
|
||||
/**
|
||||
* Check if a file is an M4A/AAC/ALAC file.
|
||||
*
|
||||
* \param file File location.
|
||||
* \return 0 on success, -1 on failure.
|
||||
*/
|
||||
int isM4a(const char* file);
|
||||
|
||||
#endif
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* ctrmus - 3DS Music Player
|
||||
* Copyright (C) 2016 Mahyar Koshkouei
|
||||
* mice - 3DS Music Player
|
||||
* 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
|
||||
@@ -9,14 +9,22 @@
|
||||
|
||||
#include <3ds.h>
|
||||
|
||||
#ifndef ctrmus_main_h
|
||||
#define ctrmus_main_h
|
||||
#ifndef mice_main_h
|
||||
#define mice_main_h
|
||||
|
||||
/* Application version */
|
||||
#define MICE_VERSION "dev63"
|
||||
|
||||
/* Default folder */
|
||||
#define DEFAULT_DIR "sdmc:/"
|
||||
|
||||
/* Maximum number of lines that can be displayed */
|
||||
/* Maximum number of lines that can be displayed on bottom screen */
|
||||
#define MAX_LIST 28
|
||||
/* Arbitrary cap for number of stored parent positions in folder to avoid
|
||||
* unbounded memory consumption. If directories are added exceeding this,
|
||||
* dequeues path closest to root to make space.
|
||||
*/
|
||||
#define MAX_DIRECTORIES 20
|
||||
|
||||
struct watchdogInfo
|
||||
{
|
||||
|
||||
44
include/metadata.h
Normal file
44
include/metadata.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef mice_metadata_h
|
||||
#define mice_metadata_h
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define METADATA_TITLE_MAX 64
|
||||
#define METADATA_ARTIST_MAX 64
|
||||
#define METADATA_ALBUM_MAX 64
|
||||
|
||||
struct metadata_t
|
||||
{
|
||||
char title[METADATA_TITLE_MAX];
|
||||
char artist[METADATA_ARTIST_MAX];
|
||||
char album[METADATA_ALBUM_MAX];
|
||||
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract metadata from an audio file
|
||||
*
|
||||
* \param file Path to the audio file
|
||||
* \param metadata Pointer to metadata structure to fill
|
||||
* \return 0 on success, -1 on failure
|
||||
*/
|
||||
int extractMetadata(const char* file, struct metadata_t* metadata);
|
||||
|
||||
/**
|
||||
* Clear metadata structure and free any allocated memory
|
||||
*
|
||||
* \param metadata Pointer to metadata structure to clear
|
||||
*/
|
||||
void clearMetadata(struct metadata_t* metadata);
|
||||
|
||||
/**
|
||||
* 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 displayMetadata(struct metadata_t* metadata, const char* filename);
|
||||
|
||||
#endif
|
||||
@@ -1,8 +1,8 @@
|
||||
#include <stdbool.h>
|
||||
#include <limits.h>
|
||||
|
||||
#ifndef ctrmus_playback_h
|
||||
#define ctrmus_playback_h
|
||||
#ifndef mice_playback_h
|
||||
#define mice_playback_h
|
||||
|
||||
/* Channel to play music on */
|
||||
#define CHANNEL 0x08
|
||||
@@ -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
|
||||
|
||||
19
increment_version.sh
Executable file
19
increment_version.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Auto-increment development version number
|
||||
VERSION_FILE="include/main.h"
|
||||
CURRENT_VERSION=$(grep "MICE_VERSION" "$VERSION_FILE" | sed -n 's/.*"dev\([0-9]*\)".*/\1/p')
|
||||
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "Could not find version number, starting at dev1"
|
||||
NEW_VERSION=1
|
||||
else
|
||||
NEW_VERSION=$((CURRENT_VERSION + 1))
|
||||
fi
|
||||
|
||||
echo "Incrementing version: dev$CURRENT_VERSION -> dev$NEW_VERSION"
|
||||
|
||||
# Update the version in the header file
|
||||
sed -i "s/dev[0-9]*/dev$NEW_VERSION/g" "$VERSION_FILE"
|
||||
|
||||
echo "Version updated to dev$NEW_VERSION"
|
||||
BIN
meta/banner.png
BIN
meta/banner.png
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.4 KiB |
BIN
meta/banner.xcf
BIN
meta/banner.xcf
Binary file not shown.
BIN
meta/icon.png
BIN
meta/icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 727 B After Width: | Height: | Size: 1.8 KiB |
BIN
meta/icon.xcf
BIN
meta/icon.xcf
Binary file not shown.
57
server/README.md
Normal file
57
server/README.md
Normal 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
1465
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
server/package.json
Normal file
20
server/package.json
Normal 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
1072
server/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
346
server/server.js
Normal file
346
server/server.js
Normal 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}
|
||||
<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;
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
/**
|
||||
* Return string describing error number. Extends strerror to include some
|
||||
* custom errors used in ctrmus.
|
||||
* custom errors used in mice.
|
||||
*
|
||||
* \param err Error number.
|
||||
*/
|
||||
char* ctrmus_strerror(int err)
|
||||
char* mice_strerror(int err)
|
||||
{
|
||||
char* error;
|
||||
|
||||
@@ -18,11 +18,11 @@ char* ctrmus_strerror(int err)
|
||||
error = "NDSP Initialisation failed.\nYou may need to dump DSP firmware.\n"
|
||||
"You can do this within Rosalina (using Luma CFW):\n"
|
||||
" 1. Press L+Down+Select\n 2. Misc. Options.\n 3. Dump DSP Firmware.\n"
|
||||
" 4. Restart ctrmus.";
|
||||
" 4. Restart mice.";
|
||||
break;
|
||||
|
||||
case DECODER_INIT_FAIL:
|
||||
error = "Unable to initialised decoder";
|
||||
error = "Unable to initialise decoder";
|
||||
break;
|
||||
|
||||
case FILE_NOT_SUPPORTED:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
393
source/gui.c
Normal file
393
source/gui.c
Normal 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
133
source/m4a.c
Normal file
@@ -0,0 +1,133 @@
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <3ds.h>
|
||||
|
||||
#include "error.h"
|
||||
#include "m4a.h"
|
||||
#include "playback.h"
|
||||
|
||||
/* TODO: Integrate proper AAC decoder library (libfaad2, fdk-aac, or minimp4) */
|
||||
/* For now, this is a stub implementation */
|
||||
|
||||
static size_t* buffSize;
|
||||
static uint32_t rate = 44100;
|
||||
static uint8_t channels = 2;
|
||||
|
||||
static int initM4a(const char* file);
|
||||
static uint32_t rateM4a(void);
|
||||
static uint8_t channelM4a(void);
|
||||
static uint64_t decodeM4a(void* buffer);
|
||||
static void exitM4a(void);
|
||||
static size_t getFileSamplesM4a(void);
|
||||
|
||||
/**
|
||||
* Set decoder parameters for M4A/AAC.
|
||||
*
|
||||
* \param decoder Structure to store parameters.
|
||||
*/
|
||||
void setM4a(struct decoder_fn* decoder)
|
||||
{
|
||||
decoder->init = &initM4a;
|
||||
decoder->rate = &rateM4a;
|
||||
decoder->channels = &channelM4a;
|
||||
buffSize = &(decoder->buffSize);
|
||||
decoder->decode = &decodeM4a;
|
||||
decoder->exit = &exitM4a;
|
||||
decoder->getFileSamples = &getFileSamplesM4a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is an M4A/AAC/ALAC file.
|
||||
*
|
||||
* \param file File location.
|
||||
* \return 0 on success, -1 on failure.
|
||||
*/
|
||||
int isM4a(const char* file)
|
||||
{
|
||||
FILE* ftest = fopen(file, "rb");
|
||||
uint32_t fileSig;
|
||||
uint32_t ftypSig;
|
||||
|
||||
if(ftest == NULL)
|
||||
return -1;
|
||||
|
||||
/* Read first 4 bytes (should be size of ftyp atom) */
|
||||
if(fread(&fileSig, 4, 1, ftest) == 0)
|
||||
{
|
||||
fclose(ftest);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Read next 4 bytes (should be 'ftyp') */
|
||||
if(fread(&ftypSig, 4, 1, ftest) == 0)
|
||||
{
|
||||
fclose(ftest);
|
||||
return -1;
|
||||
}
|
||||
|
||||
fclose(ftest);
|
||||
|
||||
/* Check for 'ftyp' signature (0x70797466 in little-endian) */
|
||||
if(ftypSig == 0x70797466)
|
||||
return 0;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int initM4a(const char* file)
|
||||
{
|
||||
(void)file;
|
||||
|
||||
/* TODO: Initialize AAC decoder */
|
||||
/* This requires:
|
||||
* 1. Parse MP4 container to find AAC audio track
|
||||
* 2. Extract decoder config (sample rate, channels, etc.)
|
||||
* 3. Initialize AAC decoder with config
|
||||
*/
|
||||
|
||||
/* Set default values for now */
|
||||
rate = 44100;
|
||||
channels = 2;
|
||||
*buffSize = rate * channels * sizeof(int16_t);
|
||||
|
||||
errno = FILE_NOT_SUPPORTED;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static uint32_t rateM4a(void)
|
||||
{
|
||||
return rate;
|
||||
}
|
||||
|
||||
static uint8_t channelM4a(void)
|
||||
{
|
||||
return channels;
|
||||
}
|
||||
|
||||
static uint64_t decodeM4a(void* buffer)
|
||||
{
|
||||
(void)buffer;
|
||||
|
||||
/* TODO: Decode AAC frame */
|
||||
/* This requires:
|
||||
* 1. Read next AAC frame from MP4 container
|
||||
* 2. Decode AAC frame to PCM samples
|
||||
* 3. Write PCM samples to buffer
|
||||
* 4. Return number of samples decoded
|
||||
*/
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void exitM4a(void)
|
||||
{
|
||||
/* TODO: Clean up AAC decoder */
|
||||
}
|
||||
|
||||
static size_t getFileSamplesM4a(void)
|
||||
{
|
||||
/* TODO: Calculate total samples from MP4 metadata */
|
||||
return 0;
|
||||
}
|
||||
436
source/main.c
436
source/main.c
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* ctrmus - 3DS Music Player
|
||||
* Copyright (C) 2016 Mahyar Koshkouei
|
||||
* mice - 3DS Music Player
|
||||
* 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
|
||||
@@ -19,24 +19,33 @@
|
||||
#include "error.h"
|
||||
#include "file.h"
|
||||
#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.
|
||||
@@ -55,19 +64,17 @@ void playbackWatchdog(void* infoIn)
|
||||
|
||||
if(*info->errInfo->error > 0)
|
||||
{
|
||||
continue;
|
||||
consoleSelect(info->screen);
|
||||
printf("Error %d: %s\n", *info->errInfo->error,
|
||||
ctrmus_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"); */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,11 +121,12 @@ 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;
|
||||
}
|
||||
|
||||
printf("Playing: %s\n", playbackInfo->file);
|
||||
/* Remove "Playing:" message that was causing display issues */
|
||||
/* printf("Playing: %s\n", playbackInfo->file); */
|
||||
playbackInfo->samples_total = 0;
|
||||
playbackInfo->samples_played = 0;
|
||||
playbackInfo->samples_per_second = 0;
|
||||
@@ -139,7 +147,7 @@ static int cmpstringp(const void *p1, const void *p2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the list of files and folders in current director to an array.
|
||||
* Store the list of files and folders in current directory to an array.
|
||||
*/
|
||||
static int getDir(struct dirList_t* dirList)
|
||||
{
|
||||
@@ -162,7 +170,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;
|
||||
@@ -175,7 +183,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;
|
||||
@@ -185,7 +193,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++;
|
||||
}
|
||||
@@ -205,59 +213,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,60 +290,53 @@ 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;
|
||||
struct playbackInfo_t playbackInfo = { 0 };
|
||||
volatile int error = 0;
|
||||
struct dirList_t dirList = { 0 };
|
||||
struct metadata_t currentMetadata = { 0 };
|
||||
|
||||
/* ignore key release of L/R if L+R or L+down was pressed */
|
||||
bool keyLComboPressed = false;
|
||||
bool keyRComboPressed = false;
|
||||
|
||||
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, 3, 50, 28);
|
||||
consoleSetWindow(&topScreenInfo, 1, 1, 50, 2);
|
||||
|
||||
consoleSelect(&bottomScreen);
|
||||
/* 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);
|
||||
|
||||
playbackInfo.errInfo = &errInfo;
|
||||
|
||||
/* position of parent folder in parent directory */
|
||||
int prevPosition[MAX_DIRECTORIES] = {0};
|
||||
int prevFrom[MAX_DIRECTORIES] = {0};
|
||||
int oldFileNum, oldFrom;
|
||||
|
||||
chdir(DEFAULT_DIR);
|
||||
chdir("MUSIC");
|
||||
|
||||
/* TODO: Not actually possible to get less than 0 */
|
||||
if(getDir(&dirList) < 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -360,25 +355,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 ctrmus */
|
||||
/* 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();
|
||||
@@ -391,11 +383,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
|
||||
@@ -405,33 +393,24 @@ 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;
|
||||
}
|
||||
}
|
||||
// if R is pressed first
|
||||
if ((kHeld & KEY_R) && (kDown & KEY_L))
|
||||
{
|
||||
if(isPlaying() == false)
|
||||
continue;
|
||||
|
||||
// /* Stop */
|
||||
// if(kDown & KEY_B)
|
||||
// {
|
||||
// stopPlayback();
|
||||
togglePlayback();
|
||||
|
||||
// /* Clear playback information. */
|
||||
// consoleSelect(&topScreenInfo);
|
||||
// consoleClear();
|
||||
// consoleSelect(&topScreenLog);
|
||||
// //consoleClear();
|
||||
|
||||
// changeFile(NULL, &playbackInfo);
|
||||
|
||||
// /* If the playback thread is currently playing, it will now
|
||||
// * stop and tell the Watchdog thread to display "Stopped".
|
||||
// */
|
||||
// continue;
|
||||
// }
|
||||
keyLComboPressed = true;
|
||||
keyRComboPressed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if((kDown & KEY_UP ||
|
||||
@@ -443,9 +422,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 ||
|
||||
@@ -457,9 +433,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 ||
|
||||
@@ -477,12 +450,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 ||
|
||||
@@ -503,9 +473,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.");
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -516,14 +483,17 @@ int main(int argc, char **argv)
|
||||
((kDown & KEY_A) && (from == 0 && fileNum == 0)))
|
||||
{
|
||||
chdir("..");
|
||||
consoleClear();
|
||||
fileMax = getDir(&dirList);
|
||||
|
||||
fileNum = 0;
|
||||
from = 0;
|
||||
|
||||
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
|
||||
err_print("Unable to list directory.");
|
||||
fileNum = prevPosition[0];
|
||||
from = prevFrom[0];
|
||||
for (int i=0; i<MAX_DIRECTORIES-1; i++) {
|
||||
prevPosition[i] = prevPosition[i+1];
|
||||
prevFrom[i] = prevFrom[i+1];
|
||||
}
|
||||
/* default to first entry */
|
||||
prevPosition[MAX_DIRECTORIES-1] = 0;
|
||||
prevFrom[MAX_DIRECTORIES-1] = 0;
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -533,22 +503,30 @@ int main(int argc, char **argv)
|
||||
if(dirList.dirNum >= fileNum)
|
||||
{
|
||||
chdir(dirList.directories[fileNum - 1]);
|
||||
consoleClear();
|
||||
fileMax = getDir(&dirList);
|
||||
|
||||
oldFileNum = fileNum;
|
||||
oldFrom = from;
|
||||
fileNum = 0;
|
||||
from = 0;
|
||||
|
||||
if(listDir(from, MAX_LIST, fileNum, dirList) < 0)
|
||||
err_print("Unable to list directory.");
|
||||
/* 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();
|
||||
consoleSelect(&topScreenLog);
|
||||
//consoleClear();
|
||||
/* Extract and display metadata */
|
||||
char fullPath[512];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
|
||||
extractMetadata(fullPath, ¤tMetadata);
|
||||
guiDisplayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
|
||||
|
||||
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
|
||||
error = 0;
|
||||
@@ -565,19 +543,21 @@ int main(int argc, char **argv)
|
||||
keyRComboPressed = false;
|
||||
}
|
||||
bool goToNextFile = (kDown & KEY_ZR) || keyRActivation;
|
||||
if (goToNextFile && fileNum < fileMax) {
|
||||
// check that next entry is a file
|
||||
if (goToNextFile && fileNum < fileMax && dirList.dirNum < fileNum+1) {
|
||||
fileNum += 1;
|
||||
if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 &&
|
||||
from < fileMax - MAX_LIST)
|
||||
from++;
|
||||
consoleSelect(&topScreenInfo);
|
||||
consoleClear();
|
||||
consoleSelect(&topScreenLog);
|
||||
//consoleClear();
|
||||
|
||||
/* Extract and display metadata */
|
||||
char fullPath[512];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
|
||||
extractMetadata(fullPath, ¤tMetadata);
|
||||
guiDisplayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
|
||||
|
||||
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
|
||||
error = 0;
|
||||
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
|
||||
@@ -588,94 +568,111 @@ int main(int argc, char **argv)
|
||||
}
|
||||
keyLComboPressed = false;
|
||||
}
|
||||
bool goToPrevFile = (kDown & KEY_ZR) || keyLActivation;
|
||||
// don't go to ../
|
||||
if (goToPrevFile && fileNum > 1) {
|
||||
bool goToPrevFile = (kDown & KEY_ZL) || keyLActivation;
|
||||
// don't go to ../ and check that previous entry is a file
|
||||
if (goToPrevFile && fileNum > 1 && dirList.dirNum < fileNum-1) {
|
||||
fileNum -= 1;
|
||||
if(fileMax - fileNum > MAX_LIST-2 && from != 0)
|
||||
from--;
|
||||
consoleSelect(&topScreenInfo);
|
||||
consoleClear();
|
||||
consoleSelect(&topScreenLog);
|
||||
//consoleClear();
|
||||
|
||||
/* Extract and display metadata */
|
||||
char fullPath[512];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
|
||||
extractMetadata(fullPath, ¤tMetadata);
|
||||
guiDisplayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
|
||||
|
||||
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
|
||||
error = 0;
|
||||
consoleSelect(&bottomScreen);
|
||||
if(listDir(from, MAX_LIST, fileNum, dirList) < 0) err_print("Unable to list directory.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// play next song automatically
|
||||
if (error == -1) {
|
||||
if (fileNum >= fileMax) {
|
||||
// don't try to play folders
|
||||
if (fileNum >= fileMax || dirList.dirNum >= fileNum) {
|
||||
error = 0;
|
||||
continue;
|
||||
}
|
||||
fileNum += 1;
|
||||
consoleSelect(&topScreenInfo);
|
||||
consoleClear();
|
||||
consoleSelect(&topScreenLog);
|
||||
//consoleClear();
|
||||
|
||||
/* Extract and display metadata */
|
||||
char fullPath[512];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
|
||||
extractMetadata(fullPath, ¤tMetadata);
|
||||
guiDisplayMetadata(¤tMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
|
||||
|
||||
changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
|
||||
error = 0;
|
||||
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(&topScreenInfo);
|
||||
/* Reset cursor position and print status. */
|
||||
printf("\033[0;0H");
|
||||
|
||||
/* 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(¤tMetadata, 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(¤tMetadata);
|
||||
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)
|
||||
{
|
||||
@@ -690,4 +687,3 @@ err:
|
||||
|
||||
goto out;
|
||||
}
|
||||
|
||||
|
||||
632
source/metadata.c
Normal file
632
source/metadata.c
Normal file
@@ -0,0 +1,632 @@
|
||||
#include <3ds.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include "metadata.h"
|
||||
#include "file.h"
|
||||
#include "all.h"
|
||||
#include "gui.h"
|
||||
|
||||
/* Internal helper functions */
|
||||
static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata);
|
||||
static int extractId3v1Metadata(FILE* fp, struct metadata_t* metadata);
|
||||
static int extractVorbisComment(FILE* fp, struct metadata_t* metadata);
|
||||
static int extractFlacMetadata(FILE* fp, struct metadata_t* metadata);
|
||||
static void trimWhitespace(char* str);
|
||||
static void parseFilename(const char* file, struct metadata_t* metadata);
|
||||
|
||||
/**
|
||||
* Extract metadata from an audio file
|
||||
*/
|
||||
int extractMetadata(const char* file, struct metadata_t* metadata)
|
||||
{
|
||||
FILE* fp;
|
||||
enum file_types fileType;
|
||||
|
||||
if(!file || !metadata)
|
||||
return -1;
|
||||
|
||||
/* Clear metadata structure */
|
||||
clearMetadata(metadata);
|
||||
|
||||
fileType = getFileType(file);
|
||||
fp = fopen(file, "rb");
|
||||
if(!fp)
|
||||
return -1;
|
||||
|
||||
switch(fileType)
|
||||
{
|
||||
case FILE_TYPE_MP3:
|
||||
/* Try ID3v2 first, then ID3v1 */
|
||||
if(extractId3v2Metadata(fp, metadata) != 0)
|
||||
extractId3v1Metadata(fp, metadata);
|
||||
break;
|
||||
|
||||
case FILE_TYPE_VORBIS:
|
||||
case FILE_TYPE_OPUS:
|
||||
extractVorbisComment(fp, metadata);
|
||||
break;
|
||||
|
||||
case FILE_TYPE_FLAC:
|
||||
extractFlacMetadata(fp, metadata);
|
||||
break;
|
||||
|
||||
case FILE_TYPE_WAV:
|
||||
case FILE_TYPE_SID:
|
||||
default:
|
||||
/* No metadata support for these formats yet */
|
||||
break;
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
|
||||
/* If no metadata was found, try to parse filename */
|
||||
if(!metadata->title[0] && !metadata->artist[0] && !metadata->album[0])
|
||||
{
|
||||
parseFilename(file, metadata);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear metadata structure and free any allocated memory
|
||||
*/
|
||||
void clearMetadata(struct metadata_t* metadata)
|
||||
{
|
||||
if(!metadata)
|
||||
return;
|
||||
|
||||
memset(metadata->title, 0, METADATA_TITLE_MAX);
|
||||
memset(metadata->artist, 0, METADATA_ARTIST_MAX);
|
||||
memset(metadata->album, 0, METADATA_ALBUM_MAX);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
guiDisplayMetadata(metadata, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ID3v2 metadata from MP3 file
|
||||
*/
|
||||
static int extractId3v2Metadata(FILE* fp, struct metadata_t* metadata)
|
||||
{
|
||||
char header[10];
|
||||
uint32_t tagSize;
|
||||
uint8_t major, minor, flags;
|
||||
|
||||
if(!fp || !metadata)
|
||||
return -1;
|
||||
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
if(fread(header, 1, 10, fp) != 10)
|
||||
return -1;
|
||||
|
||||
/* Check ID3v2 header "ID3" */
|
||||
if(memcmp(header, "ID3", 3) != 0)
|
||||
return -1;
|
||||
|
||||
major = header[3];
|
||||
minor = header[4];
|
||||
flags = header[5];
|
||||
|
||||
/* Calculate tag size (synchsafe integer) */
|
||||
tagSize = ((header[6] & 0x7F) << 21) |
|
||||
((header[7] & 0x7F) << 14) |
|
||||
((header[8] & 0x7F) << 7) |
|
||||
(header[9] & 0x7F);
|
||||
|
||||
/* Skip extended header if present */
|
||||
if(flags & 0x40)
|
||||
{
|
||||
uint32_t extSize;
|
||||
if(fread(&extSize, 4, 1, fp) != 1)
|
||||
return -1;
|
||||
/* Convert from big-endian */
|
||||
extSize = __builtin_bswap32(extSize);
|
||||
fseek(fp, extSize - 4, SEEK_CUR);
|
||||
}
|
||||
|
||||
long tagStart = ftell(fp);
|
||||
long tagEnd = tagStart + tagSize;
|
||||
|
||||
/* Parse frames */
|
||||
while(ftell(fp) < tagEnd - 10)
|
||||
{
|
||||
char frameId[5] = {0};
|
||||
uint32_t frameSize;
|
||||
uint16_t frameFlags;
|
||||
|
||||
if(fread(frameId, 1, 4, fp) != 4)
|
||||
break;
|
||||
|
||||
/* Check for padding */
|
||||
if(frameId[0] == 0)
|
||||
break;
|
||||
|
||||
if(fread(&frameSize, 4, 1, fp) != 1)
|
||||
break;
|
||||
frameSize = __builtin_bswap32(frameSize);
|
||||
|
||||
if(fread(&frameFlags, 2, 1, fp) != 1)
|
||||
break;
|
||||
|
||||
/* Handle text frames */
|
||||
if(strncmp(frameId, "TIT2", 4) == 0 || /* Title */
|
||||
strncmp(frameId, "TPE1", 4) == 0 || /* Artist */
|
||||
strncmp(frameId, "TALB", 4) == 0) /* Album */
|
||||
{
|
||||
char* frameData = malloc(frameSize + 1);
|
||||
if(!frameData)
|
||||
{
|
||||
fseek(fp, frameSize, SEEK_CUR);
|
||||
continue;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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 */
|
||||
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 */
|
||||
{
|
||||
/* Skip album art data */
|
||||
fseek(fp, frameSize, SEEK_CUR);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Skip unknown frame */
|
||||
fseek(fp, frameSize, SEEK_CUR);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ID3v1 metadata from MP3 file
|
||||
*/
|
||||
static int extractId3v1Metadata(FILE* fp, struct metadata_t* metadata)
|
||||
{
|
||||
char tag[128];
|
||||
|
||||
if(!fp || !metadata)
|
||||
return -1;
|
||||
|
||||
/* Seek to last 128 bytes */
|
||||
fseek(fp, -128, SEEK_END);
|
||||
|
||||
if(fread(tag, 1, 128, fp) != 128)
|
||||
return -1;
|
||||
|
||||
/* Check for "TAG" signature */
|
||||
if(memcmp(tag, "TAG", 3) != 0)
|
||||
return -1;
|
||||
|
||||
/* Extract fields */
|
||||
if(!metadata->title[0])
|
||||
{
|
||||
strncpy(metadata->title, tag + 3, 30);
|
||||
metadata->title[30] = 0;
|
||||
trimWhitespace(metadata->title);
|
||||
}
|
||||
|
||||
if(!metadata->artist[0])
|
||||
{
|
||||
strncpy(metadata->artist, tag + 33, 30);
|
||||
metadata->artist[30] = 0;
|
||||
trimWhitespace(metadata->artist);
|
||||
}
|
||||
|
||||
if(!metadata->album[0])
|
||||
{
|
||||
strncpy(metadata->album, tag + 63, 30);
|
||||
metadata->album[30] = 0;
|
||||
trimWhitespace(metadata->album);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Vorbis comment metadata (for OGG Vorbis/Opus)
|
||||
*/
|
||||
static int extractVorbisComment(FILE* fp, struct metadata_t* metadata)
|
||||
{
|
||||
/* This is a simplified implementation for Vorbis comments.
|
||||
* A full implementation would require parsing the OGG container structure.
|
||||
* For now, we'll try to find common Vorbis comment fields by searching for them.
|
||||
*/
|
||||
char buffer[4096];
|
||||
size_t bytesRead;
|
||||
|
||||
if(!fp || !metadata)
|
||||
return -1;
|
||||
|
||||
/* Reset to beginning */
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
|
||||
/* Read and search for Vorbis comment patterns */
|
||||
while((bytesRead = fread(buffer, 1, sizeof(buffer) - 1, fp)) > 0)
|
||||
{
|
||||
buffer[bytesRead] = '\0';
|
||||
|
||||
/* Look for TITLE= */
|
||||
char* title = strstr(buffer, "TITLE=");
|
||||
if(title && !metadata->title[0])
|
||||
{
|
||||
title += 6; /* Skip "TITLE=" */
|
||||
char* end = strchr(title, '\1'); /* Vorbis comments end with \1 or \0 */
|
||||
if(!end) end = strchr(title, '\0');
|
||||
if(end)
|
||||
{
|
||||
size_t len = end - title;
|
||||
if(len >= METADATA_TITLE_MAX) len = METADATA_TITLE_MAX - 1;
|
||||
strncpy(metadata->title, title, len);
|
||||
metadata->title[len] = '\0';
|
||||
trimWhitespace(metadata->title);
|
||||
}
|
||||
}
|
||||
|
||||
/* Look for ARTIST= or ALBUMARTIST= */
|
||||
char* artist = strstr(buffer, "ARTIST=");
|
||||
if(!artist)
|
||||
artist = strstr(buffer, "ALBUMARTIST=");
|
||||
|
||||
if(artist && !metadata->artist[0])
|
||||
{
|
||||
if(strncmp(artist, "ALBUMARTIST=", 12) == 0)
|
||||
artist += 12; /* Skip "ALBUMARTIST=" */
|
||||
else
|
||||
artist += 7; /* Skip "ARTIST=" */
|
||||
|
||||
char* end = strchr(artist, '\1');
|
||||
if(!end) end = strchr(artist, '\0');
|
||||
if(end)
|
||||
{
|
||||
size_t len = end - artist;
|
||||
if(len >= METADATA_ARTIST_MAX) len = METADATA_ARTIST_MAX - 1;
|
||||
strncpy(metadata->artist, artist, len);
|
||||
metadata->artist[len] = '\0';
|
||||
trimWhitespace(metadata->artist);
|
||||
}
|
||||
}
|
||||
|
||||
/* Look for ALBUM= */
|
||||
char* album = strstr(buffer, "ALBUM=");
|
||||
if(album && !metadata->album[0])
|
||||
{
|
||||
album += 6; /* Skip "ALBUM=" */
|
||||
char* end = strchr(album, '\1');
|
||||
if(!end) end = strchr(album, '\0');
|
||||
if(end)
|
||||
{
|
||||
size_t len = end - album;
|
||||
if(len >= METADATA_ALBUM_MAX) len = METADATA_ALBUM_MAX - 1;
|
||||
strncpy(metadata->album, album, len);
|
||||
metadata->album[len] = '\0';
|
||||
trimWhitespace(metadata->album);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract FLAC metadata
|
||||
*/
|
||||
static int extractFlacMetadata(FILE* fp, struct metadata_t* metadata)
|
||||
{
|
||||
char header[4];
|
||||
|
||||
if(!fp || !metadata)
|
||||
return -1;
|
||||
|
||||
/* Check for FLAC signature */
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
if(fread(header, 1, 4, fp) != 4 || memcmp(header, "fLaC", 4) != 0)
|
||||
return -1;
|
||||
|
||||
/* FLAC metadata blocks follow the signature */
|
||||
while(1)
|
||||
{
|
||||
uint8_t blockHeader[4];
|
||||
uint32_t blockSize;
|
||||
bool isLast;
|
||||
uint8_t blockType;
|
||||
|
||||
if(fread(blockHeader, 1, 4, fp) != 4)
|
||||
break;
|
||||
|
||||
isLast = (blockHeader[0] & 0x80) != 0;
|
||||
blockType = blockHeader[0] & 0x7F;
|
||||
blockSize = (blockHeader[1] << 16) | (blockHeader[2] << 8) | blockHeader[3];
|
||||
|
||||
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 */
|
||||
char* commentData = malloc(blockSize);
|
||||
if(commentData)
|
||||
{
|
||||
if(fread(commentData, 1, blockSize, fp) == blockSize)
|
||||
{
|
||||
/* Skip vendor string length (4 bytes) and vendor string */
|
||||
if(blockSize > 4)
|
||||
{
|
||||
uint32_t vendorLength = commentData[0] | (commentData[1] << 8) |
|
||||
(commentData[2] << 16) | (commentData[3] << 24);
|
||||
|
||||
if(vendorLength + 8 < blockSize) /* 4 bytes vendor len + vendor + 4 bytes comment count */
|
||||
{
|
||||
uint32_t commentCount = commentData[4 + vendorLength] |
|
||||
(commentData[5 + vendorLength] << 8) |
|
||||
(commentData[6 + vendorLength] << 16) |
|
||||
(commentData[7 + vendorLength] << 24);
|
||||
|
||||
uint32_t offset = 8 + vendorLength;
|
||||
for(uint32_t i = 0; i < commentCount && offset < blockSize; i++)
|
||||
{
|
||||
if(offset + 4 > blockSize) break;
|
||||
|
||||
uint32_t commentLength = commentData[offset] | (commentData[offset + 1] << 8) |
|
||||
(commentData[offset + 2] << 16) | (commentData[offset + 3] << 24);
|
||||
offset += 4;
|
||||
|
||||
if(offset + commentLength > blockSize) break;
|
||||
|
||||
/* Parse comment */
|
||||
char* comment = commentData + offset;
|
||||
if(commentLength > 0 && commentLength < 1000) /* Reasonable size check */
|
||||
{
|
||||
char tempComment[1001];
|
||||
strncpy(tempComment, comment, commentLength);
|
||||
tempComment[commentLength] = '\0';
|
||||
|
||||
if(strncmp(tempComment, "TITLE=", 6) == 0 && !metadata->title[0])
|
||||
{
|
||||
strncpy(metadata->title, tempComment + 6, METADATA_TITLE_MAX - 1);
|
||||
metadata->title[METADATA_TITLE_MAX - 1] = '\0';
|
||||
trimWhitespace(metadata->title);
|
||||
}
|
||||
else if(strncmp(tempComment, "ARTIST=", 7) == 0 && !metadata->artist[0])
|
||||
{
|
||||
strncpy(metadata->artist, tempComment + 7, METADATA_ARTIST_MAX - 1);
|
||||
metadata->artist[METADATA_ARTIST_MAX - 1] = '\0';
|
||||
trimWhitespace(metadata->artist);
|
||||
}
|
||||
else if(strncmp(tempComment, "ALBUMARTIST=", 12) == 0 && !metadata->artist[0])
|
||||
{
|
||||
strncpy(metadata->artist, tempComment + 12, METADATA_ARTIST_MAX - 1);
|
||||
metadata->artist[METADATA_ARTIST_MAX - 1] = '\0';
|
||||
trimWhitespace(metadata->artist);
|
||||
}
|
||||
else if(strncmp(tempComment, "ALBUM=", 6) == 0 && !metadata->album[0])
|
||||
{
|
||||
strncpy(metadata->album, tempComment + 6, METADATA_ALBUM_MAX - 1);
|
||||
metadata->album[METADATA_ALBUM_MAX - 1] = '\0';
|
||||
trimWhitespace(metadata->album);
|
||||
}
|
||||
}
|
||||
|
||||
offset += commentLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
free(commentData);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Skip other block types */
|
||||
fseek(fp, blockSize, SEEK_CUR);
|
||||
}
|
||||
|
||||
if(isLast)
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim leading and trailing whitespace from string
|
||||
*/
|
||||
static void trimWhitespace(char* str)
|
||||
{
|
||||
char* start = str;
|
||||
char* end;
|
||||
|
||||
if(!str || *str == 0)
|
||||
return;
|
||||
|
||||
/* Trim leading space */
|
||||
while(isspace((unsigned char)*start))
|
||||
start++;
|
||||
|
||||
if(*start == 0) /* All spaces? */
|
||||
{
|
||||
*str = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
/* Trim trailing space */
|
||||
end = start + strlen(start) - 1;
|
||||
while(end > start && isspace((unsigned char)*end))
|
||||
end--;
|
||||
|
||||
/* Write new null terminator */
|
||||
end[1] = '\0';
|
||||
|
||||
/* Move trimmed string to beginning if needed */
|
||||
if(start != str)
|
||||
{
|
||||
memmove(str, start, strlen(start) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse filename for metadata when tags are not available
|
||||
* Handles formats like "Artist - Album - Track - Title.ext"
|
||||
*/
|
||||
static void parseFilename(const char* file, struct metadata_t* metadata)
|
||||
{
|
||||
if(!file || !metadata)
|
||||
return;
|
||||
|
||||
/* Extract just the filename without path */
|
||||
const char* basename = strrchr(file, '/');
|
||||
if(!basename)
|
||||
basename = file;
|
||||
else
|
||||
basename++; /* Skip the '/' */
|
||||
|
||||
/* Remove file extension */
|
||||
char filename[256];
|
||||
strncpy(filename, basename, sizeof(filename) - 1);
|
||||
filename[sizeof(filename) - 1] = '\0';
|
||||
char* dot = strrchr(filename, '.');
|
||||
if(dot) *dot = '\0';
|
||||
|
||||
/* Look for pattern: "Artist - Album - Track - Title" */
|
||||
char* parts[4] = {NULL, NULL, NULL, NULL};
|
||||
int partCount = 0;
|
||||
|
||||
char* token = strtok(filename, " - ");
|
||||
while(token && partCount < 4)
|
||||
{
|
||||
parts[partCount] = token;
|
||||
partCount++;
|
||||
token = strtok(NULL, " - ");
|
||||
}
|
||||
|
||||
/* Assign parts based on count */
|
||||
if(partCount >= 4)
|
||||
{
|
||||
/* Artist - Album - Track - Title */
|
||||
strncpy(metadata->artist, parts[0], METADATA_ARTIST_MAX - 1);
|
||||
strncpy(metadata->album, parts[1], METADATA_ALBUM_MAX - 1);
|
||||
strncpy(metadata->title, parts[3], METADATA_TITLE_MAX - 1);
|
||||
}
|
||||
else if(partCount == 3)
|
||||
{
|
||||
/* Artist - Album - Title */
|
||||
strncpy(metadata->artist, parts[0], METADATA_ARTIST_MAX - 1);
|
||||
strncpy(metadata->album, parts[1], METADATA_ALBUM_MAX - 1);
|
||||
strncpy(metadata->title, parts[2], METADATA_TITLE_MAX - 1);
|
||||
}
|
||||
else if(partCount == 2)
|
||||
{
|
||||
/* Artist - Title */
|
||||
strncpy(metadata->artist, parts[0], METADATA_ARTIST_MAX - 1);
|
||||
strncpy(metadata->title, parts[1], METADATA_TITLE_MAX - 1);
|
||||
}
|
||||
else if(partCount == 1)
|
||||
{
|
||||
/* Just Title */
|
||||
strncpy(metadata->title, parts[0], METADATA_TITLE_MAX - 1);
|
||||
}
|
||||
|
||||
/* Ensure null termination */
|
||||
metadata->artist[METADATA_ARTIST_MAX - 1] = '\0';
|
||||
metadata->album[METADATA_ALBUM_MAX - 1] = '\0';
|
||||
metadata->title[METADATA_TITLE_MAX - 1] = '\0';
|
||||
|
||||
/* Trim whitespace */
|
||||
trimWhitespace(metadata->artist);
|
||||
trimWhitespace(metadata->album);
|
||||
trimWhitespace(metadata->title);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -18,7 +18,7 @@ static int channels = SIDEMU_STEREO;
|
||||
static int selectedSong = 0;
|
||||
static size_t buffSize = 0.5*frequency*channels; // 0.5 seconds
|
||||
|
||||
// don't change anything below - only 16 bit/sigend PCM is supported my ctrmus
|
||||
// don't change anything below - only 16 bit/sigend PCM is supported my mice
|
||||
static int sampleFormat = SIDEMU_SIGNED_PCM;
|
||||
static int bitsPerSample = SIDEMU_16BIT;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "wav.h"
|
||||
|
||||
/**
|
||||
* Test the various decoder modules in ctrmus.
|
||||
* Test the various decoder modules in mice.
|
||||
*/
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user