lots of more code

This commit is contained in:
Soph :3 2025-11-16 21:58:45 +02:00
parent 57b0832144
commit d245a92811
5 changed files with 579 additions and 175 deletions

View file

@ -28,7 +28,9 @@ find_package(SDL2 REQUIRED)
add_executable(${PROJECT_NAME} add_executable(${PROJECT_NAME}
src/main.cpp src/main.cpp
src/lib/debug/debugScreen.c src/lib/debug/debugScreen.c
src/lib/subsonic.hpp src/subsonic.hpp
src/settings.hpp
src/lib/dr_mp3.h src/lib/dr_mp3.h
src/lib/dr_flac.h src/lib/dr_flac.h
src/lib/audio.cpp src/lib/audio.cpp
@ -42,6 +44,7 @@ target_link_libraries(${PROJECT_NAME}
SceDisplay_stub SceDisplay_stub
SceAudio_stub SceAudio_stub
SceTouch_stub SceTouch_stub
SceIme_stub
${CURL_LIBRARIES} ${CURL_LIBRARIES}
${OPENSSL_LIBRARIES} ${OPENSSL_LIBRARIES}
c c

1
emulator.sh Executable file
View file

@ -0,0 +1 @@
/home/deck/Downloads/ubuntu-latest/Vita3K make/subsonic.vpk

View file

@ -1,8 +1,11 @@
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <SDL2/SDL_audio.h> #include <SDL2/SDL_audio.h>
#include <SDL2/SDL_hints.h> #include <SDL2/SDL_hints.h>
#include <SDL2/SDL_log.h>
#include <SDL2/SDL_messagebox.h> #include <SDL2/SDL_messagebox.h>
#include <SDL2/SDL_rect.h>
#include <SDL2/SDL_stdinc.h> #include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_touch.h>
#include <SDL2/SDL_ttf.h> #include <SDL2/SDL_ttf.h>
#include <SDL2/SDL_image.h> #include <SDL2/SDL_image.h>
#include <cstddef> #include <cstddef>
@ -11,9 +14,14 @@
#include <queue> #include <queue>
#include "lib/subsonic.hpp" #include "subsonic.hpp"
#include "lib/audio.hpp" #include "lib/audio.hpp"
#include "settings.hpp"
#include <map>
#include <cmath> // for fabsf if you want
#include <psp2/libime.h>
#include <psp2/ime_dialog.h>
#include <psp2/io/stat.h>
#define SCREEN_W 960 #define SCREEN_W 960
#define SCREEN_H 544 #define SCREEN_H 544
@ -44,6 +52,14 @@ struct AudioQueueEntry {
std::string id; std::string id;
bool isFlac; bool isFlac;
}; };
struct InputBox {
int x, y, w, h;
std::string text;
bool focused = false;
int caretPos = 0; // caret index
Uint32 lastBlink = 0; // for caret blinking
bool caretVisible = true;
};
std::queue<ArtRequest> gArtRequestQ; std::queue<ArtRequest> gArtRequestQ;
std::queue<ArtResult> gArtResultQ; std::queue<ArtResult> gArtResultQ;
@ -68,18 +84,16 @@ int lastY = 0;
float velocity = 0.0f; // current scrolling speed float velocity = 0.0f; // current scrolling speed
float friction = 0.92f; // deceleration factor (lower = more friction) float friction = 0.92f; // deceleration factor (lower = more friction)
bool inertial = false; // true after releasing a drag bool inertial = false; // true after releasing a drag
std::string selectedAlbumId = ""; bool settingsOpened = false;
std::vector<AudioQueueEntry> gPlayQueue; // holds Subsonic track IDs std::vector<AudioQueueEntry> gPlayQueue; // holds Subsonic track IDs
int gQueueIndex = -1; // index in queue; -1 = not playing int gQueueIndex = -1; // index in queue; -1 = not playing
const int LEFT_X = 0; const int LEFT_X = 0;
const int LEFT_Y = 60; // below the tabs const int LEFT_Y = 60; // below the tabs
const int LEFT_W = 350; const int LEFT_W = 350;
const int LEFT_H = SCREEN_H - LEFT_Y; const int LEFT_H = SCREEN_H - LEFT_Y;
#include <map>
#include <cmath> // for fabsf if you want
// Simple cache: coverArt ID -> SDL_Texture*
std::map<std::string, SDL_Texture*> gArtCache; std::map<std::string, SDL_Texture*> gArtCache;
void log(SDL_Window* win, const char* fmt, ...) { void log(SDL_Window* win, const char* fmt, ...) {
@ -253,6 +267,204 @@ void renderLeftList(
SDL_Rect dst = {LEFT_X, LEFT_Y, LEFT_W, LEFT_H}; SDL_Rect dst = {LEFT_X, LEFT_Y, LEFT_W, LEFT_H};
SDL_RenderCopy(rd, listTex, NULL, &dst); SDL_RenderCopy(rd, listTex, NULL, &dst);
} }
void drawInputBox(SDL_Renderer* rd, TTF_Font* font, InputBox& b)
{
// background
if (b.focused)
SDL_SetRenderDrawColor(rd, 80, 80, 120, 255); // focused color
else
SDL_SetRenderDrawColor(rd, 60, 60, 90, 255);
SDL_Rect r = { b.x, b.y, b.w, b.h };
SDL_RenderFillRect(rd, &r);
// text texture
SDL_Texture* tt = nullptr;
int tw = 0, th = 0;
if (!b.text.empty()) {
tt = text(rd, font, b.text.c_str(), {255,255,255});
SDL_QueryTexture(tt, NULL, NULL, &tw, &th);
}
int textX = r.x + 6; // left padding
int textY = r.y + (r.h - th) / 2;
if (tt) {
SDL_Rect h = SDL_Rect{ textX, textY, tw, th };
SDL_RenderCopy(rd, tt, NULL, &h);
}
if (tt)
SDL_DestroyTexture(tt);
// caret blinking
Uint32 now = SDL_GetTicks();
if (now - b.lastBlink > 500) {
b.caretVisible = !b.caretVisible;
b.lastBlink = now;
}
// caret drawing
if (b.focused && b.caretVisible) {
// measure text up to caret
std::string left = b.text.substr(0, b.caretPos);
int caretX = textX;
if (!left.empty()) {
TTF_SizeUTF8(font, left.c_str(), &caretX, NULL);
caretX += textX;
}
SDL_SetRenderDrawColor(rd, 255, 255, 255, 255);
SDL_RenderDrawLine(rd, caretX, r.y + 5, caretX, r.y + r.h - 5);
}
}
void utf8_to_utf16(SceWChar16* dst, const char* src, size_t max)
{
size_t di = 0;
for (size_t si = 0; src[si] && di + 1 < max; )
{
unsigned char c = src[si++];
// 1-byte ASCII fast path
if (c < 0x80) {
dst[di++] = c;
continue;
}
// 2-byte UTF-8 → UTF-16
if ((c & 0xE0) == 0xC0) {
unsigned char c2 = src[si++];
dst[di++] = ((c & 0x1F) << 6) | (c2 & 0x3F);
continue;
}
// 3-byte UTF-8 → UTF-16
if ((c & 0xF0) == 0xE0) {
unsigned char c2 = src[si++];
unsigned char c3 = src[si++];
dst[di++] = ((c & 0x0F) << 12) |
((c2 & 0x3F) << 6) |
(c3 & 0x3F);
continue;
}
// Unsupported (skip)
}
dst[di] = 0;
}
void utf16_to_utf8(const SceWChar16* src, uint8_t* dst)
{
size_t di = 0;
for (size_t si = 0; src[si]; si++)
{
uint16_t v = src[si];
if (v < 0x80) {
dst[di++] = (uint8_t)v;
}
else if (v < 0x800) {
dst[di++] = 0xC0 | (v >> 6);
dst[di++] = 0x80 | (v & 0x3F);
}
else {
dst[di++] = 0xE0 | (v >> 12);
dst[di++] = 0x80 | ((v >> 6) & 0x3F);
dst[di++] = 0x80 | (v & 0x3F);
}
}
dst[di] = 0;
}
// Global input buffer (UTF-16, where IME writes text)
static SceWChar16 gImeBuf[SCE_IME_DIALOG_MAX_TEXT_LENGTH + 1];
// Open IME dialog (set title + current text)
static void ShowIME(const char* titleUtf8, const char* initUtf8)
{
SceImeDialogParam p;
sceImeDialogParamInit(&p);
static SceWChar16 title[64];
static SceWChar16 init[256];
utf8_to_utf16(title, titleUtf8, 63);
utf8_to_utf16(init, initUtf8, 255);
p.supportedLanguages = 0;
p.languagesForced = SCE_FALSE;
p.type = SCE_IME_TYPE_DEFAULT;
p.option = 0;
p.textBoxMode = SCE_IME_DIALOG_TEXTBOX_MODE_WITH_CLEAR;
p.maxTextLength = SCE_IME_DIALOG_MAX_TEXT_LENGTH;
p.title = title;
p.initialText = init;
p.inputTextBuffer = gImeBuf;
sceImeDialogInit(&p);
}
// Poll IME; returns true when finished and fills utf8Out
static bool PollIME(char* utf8Out, int maxlen)
{
if (sceImeDialogGetStatus() != SCE_COMMON_DIALOG_STATUS_FINISHED)
return false;
SceImeDialogResult r;
SDL_memset(&r, 0, sizeof(r));
sceImeDialogGetResult(&r);
uint8_t tmp[512];
utf16_to_utf8(gImeBuf, tmp); // <-- text is in gImeBuf, not in SceImeDialogResult
SDL_strlcpy(utf8Out, (char*)tmp, maxlen);
// if you care about which button:
// if (r.button == SCE_IME_DIALOG_BUTTON_ENTER) { ... }
sceImeDialogTerm();
return true;
}
void handleInputBoxEvent(InputBox& box, const SDL_Event& e)
{
// Click = focus + open IME keyboard
if (e.type == SDL_FINGERDOWN) {
int mx = e.tfinger.x * SCREEN_W;
int my = e.tfinger.y * SCREEN_H;
bool inside = (mx >= box.x && mx <= box.x + box.w &&
my >= box.y && my <= box.y + box.h);
if (inside) {
box.focused = true;
ShowIME("testing.. testing..", box.text.c_str());
char* result;
while(true) {
if(PollIME(result, 256)) {
// Replace the entire input content
box.text = result;
box.caretPos = box.text.size();
box.lastBlink = SDL_GetTicks();
box.caretVisible = true;
box.focused = false;
break;
}
}
} else {
box.focused = false;
}
}
if (!box.focused)
return;
}
void renderRightPanel( void renderRightPanel(
SDL_Renderer* rd, SDL_Renderer* rd,
TTF_Font* fontBig, TTF_Font* fontMed, TTF_Font* fontBig, TTF_Font* fontMed,
@ -309,14 +521,6 @@ void drawButton(SDL_Renderer* rd, TTF_Font* font, Button& b) {
SDL_DestroyTexture(tt); SDL_DestroyTexture(tt);
} }
void renderControls(SDL_Renderer* rd, TTF_Font* fontSmall,
Button& b1, Button& b2, Button& b3)
{
drawButton(rd, fontSmall, b1);
drawButton(rd, fontSmall, b2);
drawButton(rd, fontSmall, b3);
}
void renderTabs(SDL_Renderer* rd, TTF_Font* fontMed, Tab tabs[3]) { void renderTabs(SDL_Renderer* rd, TTF_Font* fontMed, Tab tabs[3]) {
for (int i=0;i<3;i++){ for (int i=0;i<3;i++){
SDL_Rect r = {tabs[i].x,tabs[i].y,tabs[i].w,tabs[i].h}; SDL_Rect r = {tabs[i].x,tabs[i].y,tabs[i].w,tabs[i].h};
@ -381,32 +585,31 @@ void loadTracks(
if (maxScroll < 0) maxScroll = 0; if (maxScroll < 0) maxScroll = 0;
} }
void loadArtists(
void loadAlbums(
std::vector<Item>& items, std::vector<Item>& items,
const SubsonicClient& ss) const SubsonicClient& ss
{ ) {
std::string err; std::string err;
auto allAlbums = ss.getAllAlbums(&err); auto allArtists = ss.getArtists(&err);
if (!err.empty()) if (!err.empty())
SDL_Log("getAlbum error: %s", err.c_str()); SDL_Log("getArtists error: %s", err.c_str());
items.clear(); items.clear();
items.reserve(allAlbums.size()); items.reserve(allArtists.size());
const int artSize = 80; const int artSize = 80;
const int rowH = 100; const int rowH = 100;
const int baseY = 70; const int baseY = 70;
for (int k = 0; k < allAlbums.size(); k++) { for (int k = 0; k < allArtists.size(); k++) {
const auto& a = allAlbums[k]; const auto& a = allArtists[k];
Item it; Item it;
int y = baseY + k * rowH; int y = baseY + k * rowH;
it.art = {20, y, artSize, artSize}; it.art = {20, y, artSize, artSize};
it.title = a.name; it.title = a.name;
it.artist = a.artist; it.artist = "";
it.coverId = a.coverArt; it.coverId = a.coverArt;
it.id = a.id; it.id = a.id;
it.isTrack = false; it.isTrack = false;
@ -421,19 +624,70 @@ void loadAlbums(
maxScroll = items.size() * rowH - LEFT_H; maxScroll = items.size() * rowH - LEFT_H;
if (maxScroll < 0) maxScroll = 0; if (maxScroll < 0) maxScroll = 0;
} }
void loadAlbums(
std::vector<Item>& items,
const SubsonicClient& ss,
std::string artist = ""
) {
std::string err;
auto allAlbums = ss.getAllAlbums(&err);
if (!err.empty())
SDL_Log("getAlbum error: %s", err.c_str());
items.clear();
const int artSize = 80;
const int rowH = 100;
const int baseY = 70;
int k2 = 0;
for (int k = 0; k < allAlbums.size(); k++) {
const auto& a = allAlbums[k];
if(!artist.empty()) {
if(a.artistId != artist) {
continue;
}
}
Item it;
int y = baseY + k2 * rowH;
it.art = {20, y, artSize, artSize};
it.title = a.name;
it.artist = a.artist;
it.coverId = a.coverArt;
it.id = a.id;
it.isTrack = false;
it.titleRect = {20 + artSize + 15, y + 10, 200, 20};
it.artistRect = {20 + artSize + 15, y + 35, 200, 20};
items.push_back(it);
k2=k2+1;
}
scrollOffset = 0;
maxScroll = items.size() * rowH - LEFT_H;
if (maxScroll < 0) maxScroll = 0;
}
int main() int main()
{ {
Settings settings;
settings.load();
SubsonicClient ss( SubsonicClient ss(
"http://192.168.8.102:4747", settings.server,
"admin", settings.user,
"admin", settings.pass,
"vita-player", "vita-player",
"1.16.1" "1.16.1"
); );
SDL_setenv("VITA_DISABLE_TOUCH_BACK", "1", 1); SDL_setenv("VITA_DISABLE_TOUCH_BACK", "1", 1);
Audio_Init(); Audio_Init();
@ -491,9 +745,31 @@ int main()
int controlsCenter = sliderBar.x + sliderBar.w/2; int controlsCenter = sliderBar.x + sliderBar.w/2;
Button btnBack = { controlsCenter - 150, 340, 80, 50, "back" }; Button btnBack = { controlsCenter - 150, 340, 80, 50, "Back" };
Button btnPlay = { controlsCenter - 40, 340, 80, 50, "play" }; Button btnPlay = { controlsCenter - 40, 340, 80, 50, "Play" };
Button btnNext = { controlsCenter + 70, 340, 80, 50, "next" }; Button btnNext = { controlsCenter + 70, 340, 80, 50, "Next" };
Button btnSettings = { SCREEN_W-80, 10, 70, 50, "Settings" };
Button btnSettingsBack = { 10, 10, 80, 50, "Back" };
InputBox serverUrl {
10,10+50+10,350,30,
settings.server
};
InputBox password {
10,10+50+10+30+10,350,30,
settings.user
};
InputBox username {
10,10+50+10+30+10+30+10,350,30,
settings.pass
};
Button btnSettingsSave = {
10, 10+50+10+30+10+30+10+30+20,80,50,"Save"
};
bool running=true; bool running=true;
@ -504,6 +780,11 @@ int main()
bool touch=false; bool touch=false;
while(SDL_PollEvent(&e)){ while(SDL_PollEvent(&e)){
if(settingsOpened) {
handleInputBoxEvent(serverUrl, e);
handleInputBoxEvent(password, e);
handleInputBoxEvent(username, e);
}
if(e.type==SDL_QUIT) running=false; if(e.type==SDL_QUIT) running=false;
if (e.type == SDL_FINGERMOTION && dragging) { if (e.type == SDL_FINGERMOTION && dragging) {
int newY = e.tfinger.y * SCREEN_H; int newY = e.tfinger.y * SCREEN_H;
@ -542,8 +823,26 @@ int main()
if (touch) if (touch)
{ {
if(settingsOpened) {
SDL_Rect rB = {btnSettingsBack.x,btnSettingsBack.y,btnSettingsBack.w,btnSettingsBack.h};
SDL_Rect rS = {btnSettingsSave.x,btnSettingsSave.y,btnSettingsSave.w,btnSettingsSave.h};
if(hit(tx, ty,rB)) {
settingsOpened = false;
}
if(hit(tx,ty,rS)) {
settings.pass = password.text;
settings.server = serverUrl.text;
settings.user = username.text;
settings.save();
}
} else {
// Tabs // Tabs
for (int i=0;i<3;i++){
// THE LAST TAB IS ONLY NAVIGATED TO IN CODE!!
for (int i=0;i<2;i++){
SDL_Rect r = {tabs[i].x,tabs[i].y,tabs[i].w,tabs[i].h}; SDL_Rect r = {tabs[i].x,tabs[i].y,tabs[i].w,tabs[i].h};
if (hit(tx,ty,r)){ if (hit(tx,ty,r)){
for(int j=0;j<3;j++) tabs[j].active=false; for(int j=0;j<3;j++) tabs[j].active=false;
@ -551,7 +850,9 @@ int main()
if (strcmp(tabs[i].label, "albums") == 0) { if (strcmp(tabs[i].label, "albums") == 0) {
loadAlbums(items, ss); loadAlbums(items, ss);
} }
if (strcmp(tabs[i].label, "artists") == 0) {
loadArtists(items, ss);
}
} }
} }
@ -560,12 +861,16 @@ int main()
r.y -= scrollOffset; r.y -= scrollOffset;
if (hit(tx, ty, {LEFT_X + r.x, r.y, r.w, r.h})) { if (hit(tx, ty, {LEFT_X + r.x, r.y, r.w, r.h})) {
if (tabs[0].active) { if (tabs[0].active) {
selectedAlbumId = items[i].id; loadTracks(items, ss, items[i].id, win);
loadTracks(items, ss, selectedAlbumId, win);
// switch to Tracks tab // switch to Tracks tab
for (int j = 0; j < 3; j++) tabs[j].active = false; for (int j = 0; j < 3; j++) tabs[j].active = false;
tabs[2].active = true; tabs[2].active = true;
} else if(tabs[1].active) {
loadAlbums(items, ss, items[i].id);
for (int j = 0; j < 3; j++) tabs[j].active = false;
tabs[0].active = true;
} else if(tabs[2].active) { } else if(tabs[2].active) {
gPlayQueue.clear(); gPlayQueue.clear();
for (int t = i; t < items.size(); t++) { for (int t = i; t < items.size(); t++) {
@ -602,6 +907,10 @@ int main()
SDL_Rect rB = {btnBack.x,btnBack.y,btnBack.w,btnBack.h}; SDL_Rect rB = {btnBack.x,btnBack.y,btnBack.w,btnBack.h};
SDL_Rect rP = {btnPlay.x,btnPlay.y,btnPlay.w,btnPlay.h}; SDL_Rect rP = {btnPlay.x,btnPlay.y,btnPlay.w,btnPlay.h};
SDL_Rect rN = {btnNext.x,btnNext.y,btnNext.w,btnNext.h}; SDL_Rect rN = {btnNext.x,btnNext.y,btnNext.w,btnNext.h};
SDL_Rect rS = {btnSettings.x,btnSettings.y,btnSettings.w,btnSettings.h};
if(hit(tx, ty,rS)) {
settingsOpened = true;
}
if (hit(tx,ty,rB)) { if (hit(tx,ty,rB)) {
// Previous track in queue // Previous track in queue
@ -651,7 +960,9 @@ int main()
} }
} }
} }
}
if(!settingsOpened) {
float prog = Audio_GetProgress(); float prog = Audio_GetProgress();
if (Audio_IsPlaying()) { if (Audio_IsPlaying()) {
@ -681,16 +992,41 @@ int main()
sel = {}; sel = {};
} }
} }
}
// ------------------------- // -------------------------
// DRAW UI // DRAW UI
// ------------------------- // -------------------------
SDL_SetRenderDrawColor(rd,25,25,35,255); SDL_SetRenderDrawColor(rd,25,25,35,255);
SDL_RenderClear(rd); SDL_RenderClear(rd);
processPendingArt(rd); processPendingArt(rd);
if(settingsOpened) {
drawButton(rd, fontSmall, btnSettingsBack);
drawInputBox(rd, fontSmall, serverUrl);
drawInputBox(rd, fontSmall, username);
drawInputBox(rd, fontSmall, password);
drawButton(rd, fontSmall, btnSettingsSave);
} else {
renderTabs(rd, fontMed, tabs); renderTabs(rd, fontMed, tabs);
renderLeftList(rd, listTex, albumTex, fontSmall,
items, scrollOffset, ss);
if (!sel.id.empty()) {
renderRightPanel(rd, fontBig, fontMed,
selArt,albumTex, ss);
}
// Slider
renderSlider(rd, sliderBar, sliderVal);
// Controls
drawButton(rd, fontSmall, btnBack);
drawButton(rd, fontSmall, btnPlay);
drawButton(rd, fontSmall, btnNext);
drawButton(rd, fontSmall, btnSettings);
// INERTIA SCROLLING UPDATE // INERTIA SCROLLING UPDATE
if (inertial && !dragging) { if (inertial && !dragging) {
@ -716,19 +1052,7 @@ int main()
velocity = 0; velocity = 0;
} }
} }
renderLeftList(rd, listTex, albumTex, fontSmall,
items, scrollOffset, ss);
if (!sel.id.empty()) {
renderRightPanel(rd, fontBig, fontMed,
selArt,albumTex, ss);
} }
// Slider
renderSlider(rd, sliderBar, sliderVal);
// Controls
renderControls(rd, fontSmall, btnBack, btnPlay, btnNext);
SDL_RenderPresent(rd); SDL_RenderPresent(rd);
SDL_Delay(16); SDL_Delay(16);
} }

76
src/settings.hpp Normal file
View file

@ -0,0 +1,76 @@
#pragma once
#include <jsoncpp/json/json.h>
#include <string>
#include <sstream>
#include <psp2/io/fcntl.h>
#include <psp2/io/stat.h>
class Settings {
public:
// ---- Stored values ----
int volume = 80;
std::string server = "http://192.168.8.102:4747";
std::string user = "admin";
std::string pass = "admin";
// ---- File paths ----
static constexpr const char* PATH_DIR = "ux0:/data/subsonic_vita";
static constexpr const char* PATH_FILE = "ux0:/data/subsonic_vita/settings.json";
Settings() {}
// ================================================================
// Save to JSON file
// ================================================================
void save() const
{
sceIoMkdir(PATH_DIR, 0777);
Json::Value v;
v["volume"] = volume;
v["server"] = server;
v["user"] = user;
v["pass"] = pass;
Json::StreamWriterBuilder b;
std::string out = Json::writeString(b, v);
SceUID fd = sceIoOpen(PATH_FILE,
SCE_O_WRONLY | SCE_O_CREAT | SCE_O_TRUNC,
0666);
if (fd >= 0) {
sceIoWrite(fd, out.c_str(), out.size());
sceIoClose(fd);
}
}
// ================================================================
// Load from JSON file
// ================================================================
void load()
{
SceUID fd = sceIoOpen(PATH_FILE, SCE_O_RDONLY, 0);
if (fd < 0)
return; // no file → defaults stay
char buf[4096];
int n = sceIoRead(fd, buf, sizeof(buf) - 1);
sceIoClose(fd);
if (n <= 0)
return;
buf[n] = 0;
Json::Value root;
Json::CharReaderBuilder b;
std::string errs;
std::istringstream ss(buf);
if (!Json::parseFromStream(b, ss, &root, &errs))
return;
if (root.isMember("volume")) volume = root["volume"].asInt();
if (root.isMember("server")) server = root["server"].asString();
if (root.isMember("user")) user = root["user"].asString();
if (root.isMember("pass")) pass = root["pass"].asString();
}
};