diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9f32ca..92b2c83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,17 +3,27 @@ name: Build sys-tune and overlay on: [push] jobs: build: - + runs-on: ubuntu-latest - container: devkitpro/devkita64 + container: devkitpro/devkita64:latest steps: - - uses: actions/checkout@v1 + - name: Checkout + uses: actions/checkout@master + with: + submodules: recursive - - name: Building libnx-Ext + - name: Build run: | - make -j$(nproc) -C sys-tune/nxExt + make -j$(nproc) + mkdir -p dist/switch/.overlays + mkdir -p dist/atmosphere/contents/4200000000000000/flags + touch dist/atmosphere/contents/4200000000000000/flags/boot2.flag + cp sys-tune/sys-tune.nsp dist/atmosphere/contents/4200000000000000/exefs.nsp + cp sys-tune/toolbox.json dist/atmosphere/contents/4200000000000000/ + cp overlay/sys-tune-overlay.ovl dist/switch/.overlays/ - - name: Building sys-tune - run: | - API_VERSION=9 make -j$(nproc) + - uses: actions/upload-artifact@master + with: + name: sys-tune + path: dist diff --git a/.gitignore b/.gitignore index 7c855e7..0eea376 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ *.nacp *.npdm *.ovl -.vscode +.vscode/settings.json build dist *.zip diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..becea99 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,22 @@ +{ + "configurations": [ + { + "name": "switch", + "includePath": [ + "${default}", + "${workspaceFolder}/**", + "${DEVKITPRO}/libnx/include/", + "${DEVKITPRO}/portlibs/switch/include/", + "${workspaceFolder}/sys-tune/nxExt/include/", + "${workspaceFolder}/overlay/lib/include/", + "${workspaceFolder}/ipc/", + "${workspaceFolder}/common/" + ], + "defines": ["__SWITCH__", "WANT_MP3", "WANT_FLAC", "WANT_WAV"], + "cStandard": "gnu17", + "cppStandard": "gnu++23", + "compilerPath": "${DEVKITPRO}/devkitA64/bin/aarch64-none-elf-gcc" + } + ], + "version": 4 +} diff --git a/Makefile b/Makefile index cda66ac..4a315ae 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ export GITHASH := $(shell git rev-parse --short HEAD) -export VERSION := 1.2.2 -export API_VERSION := 3 +export VERSION := 2.0.0 +export API_VERSION := 4 export WANT_FLAC := 1 export WANT_MP3 := 1 export WANT_WAV := 1 @@ -11,8 +11,8 @@ clean: $(MAKE) -C sys-tune/nxExt clean $(MAKE) -C overlay clean $(MAKE) -C sys-tune clean - rm -rf dist - rm sys-tune-*-*.zip + -rm -r dist + -rm sys-tune-*-*.zip overlay: $(MAKE) -C overlay @@ -28,7 +28,8 @@ dist: all mkdir -p dist/atmosphere/contents/4200000000000000 cp sys-tune/sys-tune.nsp dist/atmosphere/contents/4200000000000000/exefs.nsp cp overlay/sys-tune-overlay.ovl dist/switch/.overlays/ + cp sys-tune/toolbox.json dist/atmosphere/contents/4200000000000000/ cd dist; zip -r sys-tune-$(VERSION)-$(GITHASH).zip ./**/; cd ../; - hactool -t nso sys-tune/sys-tune.nso + -hactool -t nso sys-tune/sys-tune.nso .PHONY: all overlay module diff --git a/README.md b/README.md index d7ad838..be9e22e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ You can manage playback via the Tesla overlay in the release. ## Special thanks to: - [mackron](http://mackron.github.io/) who made the awesome [audio decoders used here.](https://github.com/mackron/dr_libs/) - [WerWolv](https://werwolv.net/) for making libtesla, the UI library used for the control overlay. +- [TotalJustice](https://github.com/ITotalJustice) for bug fixes, adding some features and bad code. ## Info for developers I implemented an IPC interface accessible via service wrappers [here](/ipc/). diff --git a/common/config/config.cpp b/common/config/config.cpp new file mode 100644 index 0000000..d2e0505 --- /dev/null +++ b/common/config/config.cpp @@ -0,0 +1,114 @@ +#include "config.hpp" +#include "sdmc/sdmc.hpp" +#include "minIni/minIni.h" +#include + +namespace config { + +namespace { + +const char CONFIG_PATH[]{"/config/sys-tune/config.ini"}; +// blacklist uses it's own config file because eventually a database +// may be setup and users can easily update their blacklist by downloading +// an updated blacklist.ini. +// Also, the blacklist lookup needs to be as fast as possible +// (literally a race until the title opens audren), so a seperate, smaller file is ideal. +const char BLACKLIST_PATH[]{"/config/sys-tune/blacklist.ini"}; + +void create_config_dir() { + /* Creating directory on every set call looks sus, but the user may delete the dir */ + /* whilst the sys-mod is running and then any changes made via the overlay */ + /* is lost, which sucks. */ + sdmc::CreateFolder("/config"); + sdmc::CreateFolder("/config/sys-tune"); +} + +auto get_tid_str(u64 tid) -> const char* { + static char buf[21]{}; + std::sprintf(buf, "%016lX", tid); + return buf; +} + +} + +auto get_shuffle() -> bool { + return ini_getbool("config", "shuffle", false, CONFIG_PATH); +} + +void set_shuffle(bool value) { + create_config_dir(); + ini_putl("config", "shuffle", value, CONFIG_PATH); +} + +auto get_repeat() -> int { + return ini_getl("config", "repeat", 1, CONFIG_PATH); +} + +void set_repeat(int value) { + create_config_dir(); + ini_putl("config", "repeat", value, CONFIG_PATH); +} + +auto get_volume() -> float { + return ini_getf("config", "volume", 1.f, CONFIG_PATH); +} + +void set_volume(float value) { + create_config_dir(); + ini_putf("config", "volume", value, CONFIG_PATH); +} + +auto has_title_enabled(u64 tid) -> bool { + return ini_haskey("title", get_tid_str(tid), CONFIG_PATH); +} + +auto get_title_enabled(u64 tid) -> bool { + return ini_getbool("title", get_tid_str(tid), true, CONFIG_PATH); +} + +void set_title_enabled(u64 tid, bool value) { + create_config_dir(); + ini_putl("title", get_tid_str(tid), value, CONFIG_PATH); +} + +auto get_title_enabled_default() -> bool { + return ini_getbool("title", "default", true, CONFIG_PATH); +} + +void set_title_enabled_default(bool value) { + create_config_dir(); + ini_putl("title", "default", value, CONFIG_PATH); +} + +auto has_title_volume(u64 tid) -> bool { + return ini_haskey("volume", get_tid_str(tid), CONFIG_PATH); +} + +auto get_title_volume(u64 tid) -> float { + return ini_getf("volume", get_tid_str(tid), 1.f, CONFIG_PATH); +} + +void set_title_volume(u64 tid, float value) { + create_config_dir(); + ini_putf("volume", get_tid_str(tid), value, CONFIG_PATH); +} + +auto get_default_title_volume() -> float { + return ini_getf("config", "global_volume", 1.f, CONFIG_PATH); +} + +void set_default_title_volume(float value) { + create_config_dir(); + ini_putf("config", "global_volume", value, CONFIG_PATH); +} + +auto get_title_blacklist(u64 tid) -> bool { + return ini_getbool("blacklist", get_tid_str(tid), false, BLACKLIST_PATH); +} + +void set_title_blacklist(u64 tid, bool value) { + create_config_dir(); + ini_putl("blacklist", get_tid_str(tid), value, BLACKLIST_PATH); +} + +} diff --git a/common/config/config.hpp b/common/config/config.hpp new file mode 100644 index 0000000..c0b8f4e --- /dev/null +++ b/common/config/config.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +namespace config { + +// tune shuffle +auto get_shuffle() -> bool; +void set_shuffle(bool value); + +// tune repeat +auto get_repeat() -> int; +void set_repeat(int value); + +// tune volume +auto get_volume() -> float; +void set_volume(float value); + +// per title tune enable +auto has_title_enabled(u64 tid) -> bool; +auto get_title_enabled(u64 tid) -> bool; +void set_title_enabled(u64 tid, bool value); + +// default for tune for every title +auto get_title_enabled_default() -> bool; +void set_title_enabled_default(bool value); + +// per title volume +auto has_title_volume(u64 tid) -> bool; +auto get_title_volume(u64 tid) -> float; +void set_title_volume(u64 tid, float value); + +// default volume for every title +auto get_default_title_volume() -> float; +void set_default_title_volume(float value); + +// returns true if title causes a fatal on launch +auto get_title_blacklist(u64 tid) -> bool; +void set_title_blacklist(u64 tid, bool value); + +} diff --git a/common/minIni/minGlue.c b/common/minIni/minGlue.c new file mode 100644 index 0000000..0ef6418 --- /dev/null +++ b/common/minIni/minGlue.c @@ -0,0 +1,138 @@ +#include "minGlue.h" +#include +#include +#include + +static bool ini_open(const char* filename, struct NxFile* nxfile, u32 mode) { + Result rc = {0}; + char filename_buf[FS_MAX_PATH] = {0}; + + if (R_FAILED(rc = fsOpenSdCardFileSystem(&nxfile->system))) { + return false; + } + + strcpy(filename_buf, filename); + + if (R_FAILED(rc = fsFsOpenFile(&nxfile->system, filename_buf, mode, &nxfile->file))) { + if (mode & FsOpenMode_Write) { + if (R_FAILED(rc = fsFsCreateFile(&nxfile->system, filename_buf, 0, 0))) { + fsFsClose(&nxfile->system); + return false; + } else { + if (R_FAILED(rc = fsFsOpenFile(&nxfile->system, filename_buf, mode, &nxfile->file))) { + fsFsClose(&nxfile->system); + return false; + } + } + } else { + fsFsClose(&nxfile->system); + return false; + } + } + + nxfile->offset = 0; + return true; +} + +bool ini_openread(const char* filename, struct NxFile* nxfile) { + return ini_open(filename, nxfile, FsOpenMode_Read); +} + +bool ini_openwrite(const char* filename, struct NxFile* nxfile) { + return ini_open(filename, nxfile, FsOpenMode_Write|FsOpenMode_Append); +} + +bool ini_openrewrite(const char* filename, struct NxFile* nxfile) { + return ini_open(filename, nxfile, FsOpenMode_Read|FsOpenMode_Write|FsOpenMode_Append); +} + +bool ini_close(struct NxFile* nxfile) { + fsFileClose(&nxfile->file); + fsFsClose(&nxfile->system); + return true; +} + +bool ini_read(char* buffer, u64 size, struct NxFile* nxfile) { + u64 bytes_read = {0}; + if (R_FAILED(fsFileRead(&nxfile->file, nxfile->offset, buffer, size, FsReadOption_None, &bytes_read))) { + return false; + } + + if (!bytes_read) { + return false; + } + + char *eol = {0}; + + if ((eol = strchr(buffer, '\n')) == NULL) { + eol = strchr(buffer, '\r'); + } + + if (eol != NULL) { + *++eol = '\0'; + bytes_read = eol - buffer; + } + + nxfile->offset += bytes_read; + return true; +} + +bool ini_write(const char* buffer, struct NxFile* nxfile) { + const size_t size = strlen(buffer); + if (R_FAILED(fsFileWrite(&nxfile->file, nxfile->offset, buffer, size, FsWriteOption_None))) { + return false; + } + nxfile->offset += size; + return true; +} + +bool ini_tell(struct NxFile* nxfile, s64* pos) { + *pos = nxfile->offset; + return true; +} + +bool ini_seek(struct NxFile* nxfile, s64* pos) { + nxfile->offset = *pos; + return true; +} + +bool ini_rename(const char* src, const char* dst) { + Result rc = {0}; + FsFileSystem fs = {0}; + char src_buf[FS_MAX_PATH] = {0}; + char dst_buf[FS_MAX_PATH] = {0}; + + if (R_FAILED(rc = fsOpenSdCardFileSystem(&fs))) { + return false; + } + + strcpy(src_buf, src); + strcpy(dst_buf, dst); + rc = fsFsRenameFile(&fs, src_buf, dst_buf); + fsFsClose(&fs); + return R_SUCCEEDED(rc); +} + +bool ini_remove(const char* filename) { + Result rc = {0}; + FsFileSystem fs = {0}; + char filename_buf[FS_MAX_PATH] = {0}; + + if (R_FAILED(rc = fsOpenSdCardFileSystem(&fs))) { + return false; + } + + strcpy(filename_buf, filename); + rc = fsFsDeleteFile(&fs, filename_buf); + fsFsClose(&fs); + return R_SUCCEEDED(rc); +} + +bool ini_ftoa(char* string, INI_REAL value) { + sprintf(string,"%f",value); + return true; +} + +INI_REAL ini_atof(const char* string) { + return strtof(string, NULL); +} diff --git a/common/minIni/minGlue.h b/common/minIni/minGlue.h new file mode 100644 index 0000000..a495f03 --- /dev/null +++ b/common/minIni/minGlue.h @@ -0,0 +1,36 @@ +#pragma once + +#if defined __cplusplus +extern "C" { +#endif + +#include + +struct NxFile { + FsFile file; + FsFileSystem system; + s64 offset; +}; + +#define INI_FILETYPE struct NxFile +#define INI_FILEPOS s64 +#define INI_OPENREWRITE +#define INI_REMOVE +#define INI_REAL float + +bool ini_openread(const char* filename, struct NxFile* nxfile); +bool ini_openwrite(const char* filename, struct NxFile* nxfile); +bool ini_openrewrite(const char* filename, struct NxFile* nxfile); +bool ini_close(struct NxFile* nxfile); +bool ini_read(char* buffer, u64 size, struct NxFile* nxfile); +bool ini_write(const char* buffer, struct NxFile* nxfile); +bool ini_tell(struct NxFile* nxfile, s64* pos); +bool ini_seek(struct NxFile* nxfile, s64* pos); +bool ini_rename(const char* src, const char* dst); +bool ini_remove(const char* filename); +bool ini_ftoa(char* string, INI_REAL value); +INI_REAL ini_atof(const char* string); + +#if defined __cplusplus +} // extern "C" { +#endif diff --git a/common/minIni/minIni.c b/common/minIni/minIni.c new file mode 100644 index 0000000..90ee57b --- /dev/null +++ b/common/minIni/minIni.c @@ -0,0 +1,952 @@ +/* minIni - Multi-Platform INI file parser, suitable for embedded systems + * + * These routines are in part based on the article "Multiplatform .INI Files" + * by Joseph J. Graf in the March 1994 issue of Dr. Dobb's Journal. + * + * Copyright (c) CompuPhase, 2008-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Version: $Id: minIni.c 53 2015-01-18 13:35:11Z thiadmer.riemersma@gmail.com $ + */ + +#if (defined _UNICODE || defined __UNICODE__ || defined UNICODE) && !defined INI_ANSIONLY +# if !defined UNICODE /* for Windows */ +# define UNICODE +# endif +# if !defined _UNICODE /* for C library */ +# define _UNICODE +# endif +#endif + +#define MININI_IMPLEMENTATION +#include "minIni.h" +#if defined NDEBUG + #define assert(e) +#else + #include +#endif + +#if !defined __T || defined INI_ANSIONLY + #include + #include + #include + #define TCHAR char + #define __T(s) s + #define _tcscat strcat + #define _tcschr strchr + #define _tcscmp strcmp + #define _tcscpy strcpy + #define _tcsicmp stricmp + #define _tcslen strlen + #define _tcsncmp strncmp + #define _tcsnicmp strnicmp + #define _tcsrchr strrchr + #define _tcstol strtol + #define _tcstod strtod + #define _totupper toupper + #define _stprintf sprintf + #define _tfgets fgets + #define _tfputs fputs + #define _tfopen fopen + #define _tremove remove + #define _trename rename +#endif + +#if defined __linux || defined __linux__ + #define __LINUX__ +#elif defined FREEBSD && !defined __FreeBSD__ + #define __FreeBSD__ +#elif defined(_MSC_VER) + #pragma warning(disable: 4996) /* for Microsoft Visual C/C++ */ +#endif +#if !defined strnicmp && !defined PORTABLE_STRNICMP + #if defined __LINUX__ || defined __FreeBSD__ || defined __OpenBSD__ || defined __APPLE__ || defined __NetBSD__ || defined __DragonFly__ || defined __GNUC__ + #define strnicmp strncasecmp + #endif +#endif +#if !defined _totupper + #define _totupper toupper +#endif + +#if !defined INI_LINETERM + #if defined __LINUX__ || defined __FreeBSD__ || defined __OpenBSD__ || defined __APPLE__ || defined __NetBSD__ || defined __DragonFly__ + #define INI_LINETERM __T("\n") + #else + #define INI_LINETERM __T("\r\n") + #endif +#endif +#if !defined INI_FILETYPE + #error Missing definition for INI_FILETYPE. +#endif + +#if !defined sizearray + #define sizearray(a) (sizeof(a) / sizeof((a)[0])) +#endif + +enum quote_option { + QUOTE_NONE, + QUOTE_ENQUOTE, + QUOTE_DEQUOTE, +}; + +#if defined PORTABLE_STRNICMP +int strnicmp(const TCHAR *s1, const TCHAR *s2, size_t n) +{ + while (n-- != 0 && (*s1 || *s2)) { + register int c1, c2; + c1 = *s1++; + if ('a' <= c1 && c1 <= 'z') + c1 += ('A' - 'a'); + c2 = *s2++; + if ('a' <= c2 && c2 <= 'z') + c2 += ('A' - 'a'); + if (c1 != c2) + return c1 - c2; + } + return 0; +} +#endif /* PORTABLE_STRNICMP */ + +static TCHAR *skipleading(const TCHAR *str) +{ + assert(str != NULL); + while ('\0' < *str && *str <= ' ') + str++; + return (TCHAR *)str; +} + +static TCHAR *skiptrailing(const TCHAR *str, const TCHAR *base) +{ + assert(str != NULL); + assert(base != NULL); + while (str > base && '\0' < *(str-1) && *(str-1) <= ' ') + str--; + return (TCHAR *)str; +} + +static TCHAR *striptrailing(TCHAR *str) +{ + TCHAR *ptr = skiptrailing(_tcschr(str, '\0'), str); + assert(ptr != NULL); + *ptr = '\0'; + return str; +} + +static TCHAR *ini_strncpy(TCHAR *dest, const TCHAR *source, size_t maxlen, enum quote_option option) +{ + size_t d, s; + + assert(maxlen>0); + assert(source != NULL && dest != NULL); + assert((dest < source || (dest == source && option != QUOTE_ENQUOTE)) || dest > source + strlen(source)); + if (option == QUOTE_ENQUOTE && maxlen < 3) + option = QUOTE_NONE; /* cannot store two quotes and a terminating zero in less than 3 characters */ + + switch (option) { + case QUOTE_NONE: + for (d = 0; d < maxlen - 1 && source[d] != '\0'; d++) + dest[d] = source[d]; + assert(d < maxlen); + dest[d] = '\0'; + break; + case QUOTE_ENQUOTE: + d = 0; + dest[d++] = '"'; + for (s = 0; source[s] != '\0' && d < maxlen - 2; s++, d++) { + if (source[s] == '"') { + if (d >= maxlen - 3) + break; /* no space to store the escape character plus the one that follows it */ + dest[d++] = '\\'; + } + dest[d] = source[s]; + } + dest[d++] = '"'; + dest[d] = '\0'; + break; + case QUOTE_DEQUOTE: + for (d = s = 0; source[s] != '\0' && d < maxlen - 1; s++, d++) { + if ((source[s] == '"' || source[s] == '\\') && source[s + 1] == '"') + s++; + dest[d] = source[s]; + } + dest[d] = '\0'; + break; + default: + assert(0); + } + + return dest; +} + +static TCHAR *cleanstring(TCHAR *string, enum quote_option *quotes) +{ + int isstring; + TCHAR *ep; + + assert(string != NULL); + assert(quotes != NULL); + + /* Remove a trailing comment */ + isstring = 0; + for (ep = string; *ep != '\0' && ((*ep != ';' && *ep != '#') || isstring); ep++) { + if (*ep == '"') { + if (*(ep + 1) == '"') + ep++; /* skip "" (both quotes) */ + else + isstring = !isstring; /* single quote, toggle isstring */ + } else if (*ep == '\\' && *(ep + 1) == '"') { + ep++; /* skip \" (both quotes */ + } + } + assert(ep != NULL && (*ep == '\0' || *ep == ';' || *ep == '#')); + *ep = '\0'; /* terminate at a comment */ + striptrailing(string); + /* Remove double quotes surrounding a value */ + *quotes = QUOTE_NONE; + if (*string == '"' && (ep = _tcschr(string, '\0')) != NULL && *(ep - 1) == '"') { + string++; + *--ep = '\0'; + *quotes = QUOTE_DEQUOTE; /* this is a string, so remove escaped characters */ + } + return string; +} + +static int getkeystring(INI_FILETYPE *fp, const TCHAR *Section, const TCHAR *Key, + int idxSection, int idxKey, TCHAR *Buffer, int BufferSize, + INI_FILEPOS *mark) +{ + TCHAR *sp, *ep; + int len, idx; + enum quote_option quotes; + TCHAR LocalBuffer[INI_BUFFERSIZE]; + + assert(fp != NULL); + /* Move through file 1 line at a time until a section is matched or EOF. If + * parameter Section is NULL, only look at keys above the first section. If + * idxSection is positive, copy the relevant section name. + */ + len = (Section != NULL) ? (int)_tcslen(Section) : 0; + if (len > 0 || idxSection >= 0) { + assert(idxSection >= 0 || Section != NULL); + idx = -1; + do { + do { + if (!ini_read(LocalBuffer, INI_BUFFERSIZE, fp)) + return 0; + sp = skipleading(LocalBuffer); + ep = _tcsrchr(sp, ']'); + } while (*sp != '[' || ep == NULL); + /* When arrived here, a section was found; now optionally skip leading and + * trailing whitespace. + */ + assert(sp != NULL && *sp == '['); + sp = skipleading(sp + 1); + assert(ep != NULL && *ep == ']'); + ep = skiptrailing(ep, sp); + } while ((((int)(ep-sp) != len || Section == NULL || _tcsnicmp(sp, Section, len) != 0) && ++idx != idxSection)); + if (idxSection >= 0) { + if (idx == idxSection) { + assert(ep != NULL); + *ep = '\0'; /* the end of the section name was found earlier */ + ini_strncpy(Buffer, sp, BufferSize, QUOTE_NONE); + return 1; + } + return 0; /* no more section found */ + } + } + + /* Now that the section has been found, find the entry. + * Stop searching upon leaving the section's area. + */ + assert(Key != NULL || idxKey >= 0); + len = (Key != NULL) ? (int)_tcslen(Key) : 0; + idx = -1; + do { + if (mark != NULL) + ini_tell(fp, mark); /* optionally keep the mark to the start of the line */ + if (!ini_read(LocalBuffer,INI_BUFFERSIZE,fp) || *(sp = skipleading(LocalBuffer)) == '[') + return 0; + sp = skipleading(LocalBuffer); + ep = _tcschr(sp, '='); /* Parse out the equal sign */ + if (ep == NULL) + ep = _tcschr(sp, ':'); + } while (*sp == ';' || *sp == '#' || ep == NULL + || ((len == 0 || (int)(skiptrailing(ep,sp)-sp) != len || _tcsnicmp(sp,Key,len) != 0) && ++idx != idxKey)); + if (idxKey >= 0) { + if (idx == idxKey) { + assert(ep != NULL); + assert(*ep == '=' || *ep == ':'); + *ep = '\0'; + striptrailing(sp); + ini_strncpy(Buffer, sp, BufferSize, QUOTE_NONE); + return 1; + } + return 0; /* no more key found (in this section) */ + } + + /* Copy up to BufferSize chars to buffer */ + assert(ep != NULL); + assert(*ep == '=' || *ep == ':'); + sp = skipleading(ep + 1); + sp = cleanstring(sp, "es); /* Remove a trailing comment */ + ini_strncpy(Buffer, sp, BufferSize, quotes); + return 1; +} + +/** ini_gets() + * \param Section the name of the section to search for + * \param Key the name of the entry to find the value of + * \param DefValue default string in the event of a failed read + * \param Buffer a pointer to the buffer to copy into + * \param BufferSize the maximum number of characters to copy + * \param Filename the name and full path of the .ini file to read from + * + * \return the number of characters copied into the supplied buffer + */ +int ini_gets(const TCHAR *Section, const TCHAR *Key, const TCHAR *DefValue, + TCHAR *Buffer, int BufferSize, const TCHAR *Filename) +{ + INI_FILETYPE fp; + int ok = 0; + + if (Buffer == NULL || BufferSize <= 0 || Key == NULL) + return 0; + if (ini_openread(Filename, &fp)) { + ok = getkeystring(&fp, Section, Key, -1, -1, Buffer, BufferSize, NULL); + (void)ini_close(&fp); + } + if (!ok) + ini_strncpy(Buffer, (DefValue != NULL) ? DefValue : __T(""), BufferSize, QUOTE_NONE); + return (int)_tcslen(Buffer); +} + +/** ini_getl() + * \param Section the name of the section to search for + * \param Key the name of the entry to find the value of + * \param DefValue the default value in the event of a failed read + * \param Filename the name of the .ini file to read from + * + * \return the value located at Key + */ +long ini_getl(const TCHAR *Section, const TCHAR *Key, long DefValue, const TCHAR *Filename) +{ + TCHAR LocalBuffer[64]; + int len = ini_gets(Section, Key, __T(""), LocalBuffer, sizearray(LocalBuffer), Filename); + return (len == 0) ? DefValue + : ((len >= 2 && _totupper((int)LocalBuffer[1]) == 'X') ? _tcstol(LocalBuffer, NULL, 16) + : _tcstol(LocalBuffer, NULL, 10)); +} + +#if defined INI_REAL +/** ini_getf() + * \param Section the name of the section to search for + * \param Key the name of the entry to find the value of + * \param DefValue the default value in the event of a failed read + * \param Filename the name of the .ini file to read from + * + * \return the value located at Key + */ +INI_REAL ini_getf(const TCHAR *Section, const TCHAR *Key, INI_REAL DefValue, const TCHAR *Filename) +{ + TCHAR LocalBuffer[64]; + int len = ini_gets(Section, Key, __T(""), LocalBuffer, sizearray(LocalBuffer), Filename); + return (len == 0) ? DefValue : ini_atof(LocalBuffer); +} +#endif + +/** ini_getbool() + * \param Section the name of the section to search for + * \param Key the name of the entry to find the value of + * \param DefValue default value in the event of a failed read; it should + * zero (0) or one (1). + * \param Filename the name and full path of the .ini file to read from + * + * A true boolean is found if one of the following is matched: + * - A string starting with 'y' or 'Y' + * - A string starting with 't' or 'T' + * - A string starting with '1' + * + * A false boolean is found if one of the following is matched: + * - A string starting with 'n' or 'N' + * - A string starting with 'f' or 'F' + * - A string starting with '0' + * + * \return the true/false flag as interpreted at Key + */ +int ini_getbool(const TCHAR *Section, const TCHAR *Key, int DefValue, const TCHAR *Filename) +{ + TCHAR LocalBuffer[2] = __T(""); + int ret; + + ini_gets(Section, Key, __T(""), LocalBuffer, sizearray(LocalBuffer), Filename); + LocalBuffer[0] = (TCHAR)_totupper((int)LocalBuffer[0]); + if (LocalBuffer[0] == 'Y' || LocalBuffer[0] == '1' || LocalBuffer[0] == 'T') + ret = 1; + else if (LocalBuffer[0] == 'N' || LocalBuffer[0] == '0' || LocalBuffer[0] == 'F') + ret = 0; + else + ret = DefValue; + + return(ret); +} + +/** ini_getsection() + * \param idx the zero-based sequence number of the section to return + * \param Buffer a pointer to the buffer to copy into + * \param BufferSize the maximum number of characters to copy + * \param Filename the name and full path of the .ini file to read from + * + * \return the number of characters copied into the supplied buffer + */ +int ini_getsection(int idx, TCHAR *Buffer, int BufferSize, const TCHAR *Filename) +{ + INI_FILETYPE fp; + int ok = 0; + + if (Buffer == NULL || BufferSize <= 0 || idx < 0) + return 0; + if (ini_openread(Filename, &fp)) { + ok = getkeystring(&fp, NULL, NULL, idx, -1, Buffer, BufferSize, NULL); + (void)ini_close(&fp); + } + if (!ok) + *Buffer = '\0'; + return (int)_tcslen(Buffer); +} + +/** ini_getkey() + * \param Section the name of the section to browse through, or NULL to + * browse through the keys outside any section + * \param idx the zero-based sequence number of the key to return + * \param Buffer a pointer to the buffer to copy into + * \param BufferSize the maximum number of characters to copy + * \param Filename the name and full path of the .ini file to read from + * + * \return the number of characters copied into the supplied buffer + */ +int ini_getkey(const TCHAR *Section, int idx, TCHAR *Buffer, int BufferSize, const TCHAR *Filename) +{ + INI_FILETYPE fp; + int ok = 0; + + if (Buffer == NULL || BufferSize <= 0 || idx < 0) + return 0; + if (ini_openread(Filename, &fp)) { + ok = getkeystring(&fp, Section, NULL, -1, idx, Buffer, BufferSize, NULL); + (void)ini_close(&fp); + } + if (!ok) + *Buffer = '\0'; + return (int)_tcslen(Buffer); +} + +/** ini_hassection() + * \param Section the name of the section to search for + * \param Filename the name of the .ini file to read from + * + * \return 1 if the section is found, 0 if not found + */ +int ini_hassection(const mTCHAR *Section, const mTCHAR *Filename) +{ + TCHAR LocalBuffer[32]; /* dummy buffer */ + INI_FILETYPE fp; + int ok = 0; + + if (ini_openread(Filename, &fp)) { + ok = getkeystring(&fp, Section, NULL, -1, 0, LocalBuffer, sizearray(LocalBuffer), NULL); + (void)ini_close(&fp); + } + return ok; +} + +/** ini_haskey() + * \param Section the name of the section to search for + * \param Key the name of the entry to find the value of + * \param Filename the name of the .ini file to read from + * + * \return 1 if the key is found, 0 if not found + */ +int ini_haskey(const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Filename) +{ + TCHAR LocalBuffer[32]; /* dummy buffer */ + INI_FILETYPE fp; + int ok = 0; + + if (ini_openread(Filename, &fp)) { + ok = getkeystring(&fp, Section, Key, -1, -1, LocalBuffer, sizearray(LocalBuffer), NULL); + (void)ini_close(&fp); + } + return ok; +} + + +#if !defined INI_NOBROWSE +/** ini_browse() + * \param Callback a pointer to a function that will be called for every + * setting in the INI file. + * \param UserData arbitrary data, which the function passes on the + * \c Callback function + * \param Filename the name and full path of the .ini file to read from + * + * \return 1 on success, 0 on failure (INI file not found) + * + * \note The \c Callback function must return 1 to continue + * browsing through the INI file, or 0 to stop. Even when the + * callback stops the browsing, this function will return 1 + * (for success). + */ +int ini_browse(INI_CALLBACK Callback, void *UserData, const TCHAR *Filename) +{ + TCHAR LocalBuffer[INI_BUFFERSIZE]; + int lenSec, lenKey; + enum quote_option quotes; + INI_FILETYPE fp; + + if (Callback == NULL) + return 0; + if (!ini_openread(Filename, &fp)) + return 0; + + LocalBuffer[0] = '\0'; /* copy an empty section in the buffer */ + lenSec = (int)_tcslen(LocalBuffer) + 1; + for ( ;; ) { + TCHAR *sp, *ep; + if (!ini_read(LocalBuffer + lenSec, INI_BUFFERSIZE - lenSec, &fp)) + break; + sp = skipleading(LocalBuffer + lenSec); + /* ignore empty strings and comments */ + if (*sp == '\0' || *sp == ';' || *sp == '#') + continue; + /* see whether we reached a new section */ + ep = _tcsrchr(sp, ']'); + if (*sp == '[' && ep != NULL) { + sp = skipleading(sp + 1); + ep = skiptrailing(ep, sp); + *ep = '\0'; + ini_strncpy(LocalBuffer, sp, INI_BUFFERSIZE, QUOTE_NONE); + lenSec = (int)_tcslen(LocalBuffer) + 1; + continue; + } + /* not a new section, test for a key/value pair */ + ep = _tcschr(sp, '='); /* test for the equal sign or colon */ + if (ep == NULL) + ep = _tcschr(sp, ':'); + if (ep == NULL) + continue; /* invalid line, ignore */ + *ep++ = '\0'; /* split the key from the value */ + striptrailing(sp); + ini_strncpy(LocalBuffer + lenSec, sp, INI_BUFFERSIZE - lenSec, QUOTE_NONE); + lenKey = (int)_tcslen(LocalBuffer + lenSec) + 1; + /* clean up the value */ + sp = skipleading(ep); + sp = cleanstring(sp, "es); /* Remove a trailing comment */ + ini_strncpy(LocalBuffer + lenSec + lenKey, sp, INI_BUFFERSIZE - lenSec - lenKey, quotes); + /* call the callback */ + if (!Callback(LocalBuffer, LocalBuffer + lenSec, LocalBuffer + lenSec + lenKey, UserData)) + break; + } + + (void)ini_close(&fp); + return 1; +} +#endif /* INI_NOBROWSE */ + +#if ! defined INI_READONLY +static void ini_tempname(TCHAR *dest, const TCHAR *source, int maxlength) +{ + TCHAR *p; + + ini_strncpy(dest, source, maxlength, QUOTE_NONE); + p = _tcschr(dest, '\0'); + assert(p != NULL); + *(p - 1) = '~'; +} + +static enum quote_option check_enquote(const TCHAR *Value) +{ + const TCHAR *p; + + /* run through the value, if it has trailing spaces, or '"', ';' or '#' + * characters, enquote it + */ + assert(Value != NULL); + for (p = Value; *p != '\0' && *p != '"' && *p != ';' && *p != '#'; p++) + /* nothing */; + return (*p != '\0' || (p > Value && *(p - 1) == ' ')) ? QUOTE_ENQUOTE : QUOTE_NONE; +} + +static void writesection(TCHAR *LocalBuffer, const TCHAR *Section, INI_FILETYPE *fp) +{ + if (Section != NULL && _tcslen(Section) > 0) { + TCHAR *p; + LocalBuffer[0] = '['; + ini_strncpy(LocalBuffer + 1, Section, INI_BUFFERSIZE - 4, QUOTE_NONE); /* -1 for '[', -1 for ']', -2 for '\r\n' */ + p = _tcschr(LocalBuffer, '\0'); + assert(p != NULL); + *p++ = ']'; + _tcscpy(p, INI_LINETERM); /* copy line terminator (typically "\n") */ + if (fp != NULL) + (void)ini_write(LocalBuffer, fp); + } +} + +static void writekey(TCHAR *LocalBuffer, const TCHAR *Key, const TCHAR *Value, INI_FILETYPE *fp) +{ + TCHAR *p; + enum quote_option option = check_enquote(Value); + ini_strncpy(LocalBuffer, Key, INI_BUFFERSIZE - 3, QUOTE_NONE); /* -1 for '=', -2 for '\r\n' */ + p = _tcschr(LocalBuffer, '\0'); + assert(p != NULL); + *p++ = '='; + ini_strncpy(p, Value, INI_BUFFERSIZE - (p - LocalBuffer) - 2, option); /* -2 for '\r\n' */ + p = _tcschr(LocalBuffer, '\0'); + assert(p != NULL); + _tcscpy(p, INI_LINETERM); /* copy line terminator (typically "\n") */ + if (fp != NULL) + (void)ini_write(LocalBuffer, fp); +} + +static int cache_accum(const TCHAR *string, int *size, int max) +{ + int len = (int)_tcslen(string); + if (*size + len >= max) + return 0; + *size += len; + return 1; +} + +static int cache_flush(TCHAR *buffer, int *size, + INI_FILETYPE *rfp, INI_FILETYPE *wfp, INI_FILEPOS *mark) +{ + int terminator_len = (int)_tcslen(INI_LINETERM); + int pos = 0, pos_prev = -1; + + (void)ini_seek(rfp, mark); + assert(buffer != NULL); + buffer[0] = '\0'; + assert(size != NULL); + assert(*size <= INI_BUFFERSIZE); + while (pos < *size && pos != pos_prev) { + pos_prev = pos; /* to guard against zero bytes in the INI file */ + (void)ini_read(buffer + pos, INI_BUFFERSIZE - pos, rfp); + while (pos < *size && buffer[pos] != '\0') + pos++; /* cannot use _tcslen() because buffer may not be zero-terminated */ + } + if (buffer[0] != '\0') { + assert(pos > 0 && pos <= INI_BUFFERSIZE); + if (pos == INI_BUFFERSIZE) + pos--; + buffer[pos] = '\0'; /* force zero-termination (may be left unterminated in the above while loop) */ + (void)ini_write(buffer, wfp); + } + ini_tell(rfp, mark); /* update mark */ + *size = 0; + /* return whether the buffer ended with a line termination */ + return (pos > terminator_len) && (_tcscmp(buffer + pos - terminator_len, INI_LINETERM) == 0); +} + +static int close_rename(INI_FILETYPE *rfp, INI_FILETYPE *wfp, const TCHAR *filename, TCHAR *buffer) +{ + (void)ini_close(rfp); + (void)ini_close(wfp); + (void)ini_tempname(buffer, filename, INI_BUFFERSIZE); + #if defined ini_remove || defined INI_REMOVE + (void)ini_remove(filename); + #endif + (void)ini_rename(buffer, filename); + return 1; +} + +/** ini_puts() + * \param Section the name of the section to write the string in + * \param Key the name of the entry to write, or NULL to erase all keys in the section + * \param Value a pointer to the buffer the string, or NULL to erase the key + * \param Filename the name and full path of the .ini file to write to + * + * \return 1 if successful, otherwise 0 + */ +int ini_puts(const TCHAR *Section, const TCHAR *Key, const TCHAR *Value, const TCHAR *Filename) +{ + INI_FILETYPE rfp; + INI_FILETYPE wfp; + INI_FILEPOS mark; + INI_FILEPOS head, tail; + TCHAR *sp, *ep; + TCHAR LocalBuffer[INI_BUFFERSIZE]; + int len, match, flag, cachelen; + + assert(Filename != NULL); + if (!ini_openread(Filename, &rfp)) { + /* If the .ini file doesn't exist, make a new file */ + if (Key != NULL && Value != NULL) { + if (!ini_openwrite(Filename, &wfp)) + return 0; + writesection(LocalBuffer, Section, &wfp); + writekey(LocalBuffer, Key, Value, &wfp); + (void)ini_close(&wfp); + } + return 1; + } + + /* If parameters Key and Value are valid (so this is not an "erase" request) + * and the setting already exists, there are two short-cuts to avoid rewriting + * the INI file. + */ + if (Key != NULL && Value != NULL) { + match = getkeystring(&rfp, Section, Key, -1, -1, LocalBuffer, sizearray(LocalBuffer), &head); + if (match) { + /* if the current setting is identical to the one to write, there is + * nothing to do. + */ + if (_tcscmp(LocalBuffer,Value) == 0) { + (void)ini_close(&rfp); + return 1; + } + /* if the new setting has the same length as the current setting, and the + * glue file permits file read/write access, we can modify in place. + */ + #if defined ini_openrewrite || defined INI_OPENREWRITE + /* we already have the start of the (raw) line, get the end too */ + ini_tell(&rfp, &tail); + /* create new buffer (without writing it to file) */ + writekey(LocalBuffer, Key, Value, NULL); + if (_tcslen(LocalBuffer) == (size_t)(tail - head)) { + /* length matches, close the file & re-open for read/write, then + * write at the correct position + */ + (void)ini_close(&rfp); + if (!ini_openrewrite(Filename, &wfp)) + return 0; + (void)ini_seek(&wfp, &head); + (void)ini_write(LocalBuffer, &wfp); + (void)ini_close(&wfp); + return 1; + } + #endif + } + /* key not found, or different value & length -> proceed */ + } else if (Key != NULL && Value == NULL) { + /* Conversely, for a request to delete a setting; if that setting isn't + present, just return */ + match = getkeystring(&rfp, Section, Key, -1, -1, LocalBuffer, sizearray(LocalBuffer), NULL); + if (!match) { + (void)ini_close(&rfp); + return 1; + } + /* key found -> proceed to delete it */ + } + + /* Get a temporary file name to copy to. Use the existing name, but with + * the last character set to a '~'. + */ + (void)ini_close(&rfp); + ini_tempname(LocalBuffer, Filename, INI_BUFFERSIZE); + if (!ini_openwrite(LocalBuffer, &wfp)) + return 0; + /* In the case of (advisory) file locks, ini_openwrite() may have been blocked + * on the open, and after the block is lifted, the original file may have been + * renamed, which is why the original file was closed and is now reopened */ + if (!ini_openread(Filename, &rfp)) { + /* If the .ini file doesn't exist any more, make a new file */ + assert(Key != NULL && Value != NULL); + writesection(LocalBuffer, Section, &wfp); + writekey(LocalBuffer, Key, Value, &wfp); + (void)ini_close(&wfp); + return 1; + } + + (void)ini_tell(&rfp, &mark); + cachelen = 0; + + /* Move through the file one line at a time until a section is + * matched or until EOF. Copy to temp file as it is read. + */ + len = (Section != NULL) ? (int)_tcslen(Section) : 0; + if (len > 0) { + do { + if (!ini_read(LocalBuffer, INI_BUFFERSIZE, &rfp)) { + /* Failed to find section, so add one to the end */ + flag = cache_flush(LocalBuffer, &cachelen, &rfp, &wfp, &mark); + if (Key!=NULL && Value!=NULL) { + if (!flag) + (void)ini_write(INI_LINETERM, &wfp); /* force a new line behind the last line of the INI file */ + writesection(LocalBuffer, Section, &wfp); + writekey(LocalBuffer, Key, Value, &wfp); + } + return close_rename(&rfp, &wfp, Filename, LocalBuffer); /* clean up and rename */ + } + /* Check whether this line is a section */ + sp = skipleading(LocalBuffer); + ep = _tcsrchr(sp, ']'); + match = (*sp == '[' && ep != NULL); + if (match) { + /* A section was found, skip leading and trailing whitespace */ + assert(sp != NULL && *sp == '['); + sp = skipleading(sp + 1); + assert(ep != NULL && *ep == ']'); + ep = skiptrailing(ep, sp); + match = ((int)(ep-sp) == len && _tcsnicmp(sp, Section, len) == 0); + } + /* Copy the line from source to dest, but not if this is the section that + * we are looking for and this section must be removed + */ + if (!match || Key != NULL) { + if (!cache_accum(LocalBuffer, &cachelen, INI_BUFFERSIZE)) { + cache_flush(LocalBuffer, &cachelen, &rfp, &wfp, &mark); + (void)ini_read(LocalBuffer, INI_BUFFERSIZE, &rfp); + cache_accum(LocalBuffer, &cachelen, INI_BUFFERSIZE); + } + } + } while (!match); + } + cache_flush(LocalBuffer, &cachelen, &rfp, &wfp, &mark); + /* when deleting a section, the section head that was just found has not been + * copied to the output file, but because this line was not "accumulated" in + * the cache, the position in the input file was reset to the point just + * before the section; this must now be skipped (again) + */ + if (Key == NULL) { + (void)ini_read(LocalBuffer, INI_BUFFERSIZE, &rfp); + (void)ini_tell(&rfp, &mark); + } + + /* Now that the section has been found, find the entry. Stop searching + * upon leaving the section's area. Copy the file as it is read + * and create an entry if one is not found. + */ + len = (Key != NULL) ? (int)_tcslen(Key) : 0; + for( ;; ) { + if (!ini_read(LocalBuffer, INI_BUFFERSIZE, &rfp)) { + /* EOF without an entry so make one */ + flag = cache_flush(LocalBuffer, &cachelen, &rfp, &wfp, &mark); + if (Key!=NULL && Value!=NULL) { + if (!flag) + (void)ini_write(INI_LINETERM, &wfp); /* force a new line behind the last line of the INI file */ + writekey(LocalBuffer, Key, Value, &wfp); + } + return close_rename(&rfp, &wfp, Filename, LocalBuffer); /* clean up and rename */ + } + sp = skipleading(LocalBuffer); + ep = _tcschr(sp, '='); /* Parse out the equal sign */ + if (ep == NULL) + ep = _tcschr(sp, ':'); + match = (ep != NULL && len > 0 && (int)(skiptrailing(ep,sp)-sp) == len && _tcsnicmp(sp,Key,len) == 0); + if ((Key != NULL && match) || *sp == '[') + break; /* found the key, or found a new section */ + /* copy other keys in the section */ + if (Key == NULL) { + (void)ini_tell(&rfp, &mark); /* we are deleting the entire section, so update the read position */ + } else { + if (!cache_accum(LocalBuffer, &cachelen, INI_BUFFERSIZE)) { + cache_flush(LocalBuffer, &cachelen, &rfp, &wfp, &mark); + (void)ini_read(LocalBuffer, INI_BUFFERSIZE, &rfp); + cache_accum(LocalBuffer, &cachelen, INI_BUFFERSIZE); + } + } + } + /* the key was found, or we just dropped on the next section (meaning that it + * wasn't found); in both cases we need to write the key, but in the latter + * case, we also need to write the line starting the new section after writing + * the key + */ + flag = (*sp == '['); + cache_flush(LocalBuffer, &cachelen, &rfp, &wfp, &mark); + if (Key != NULL && Value != NULL) + writekey(LocalBuffer, Key, Value, &wfp); + /* cache_flush() reset the "read pointer" to the start of the line with the + * previous key or the new section; read it again (because writekey() destroyed + * the buffer) + */ + (void)ini_read(LocalBuffer, INI_BUFFERSIZE, &rfp); + if (flag) { + /* the new section heading needs to be copied to the output file */ + cache_accum(LocalBuffer, &cachelen, INI_BUFFERSIZE); + } else { + /* forget the old key line */ + (void)ini_tell(&rfp, &mark); + } + /* Copy the rest of the INI file */ + while (ini_read(LocalBuffer, INI_BUFFERSIZE, &rfp)) { + if (!cache_accum(LocalBuffer, &cachelen, INI_BUFFERSIZE)) { + cache_flush(LocalBuffer, &cachelen, &rfp, &wfp, &mark); + (void)ini_read(LocalBuffer, INI_BUFFERSIZE, &rfp); + cache_accum(LocalBuffer, &cachelen, INI_BUFFERSIZE); + } + } + cache_flush(LocalBuffer, &cachelen, &rfp, &wfp, &mark); + return close_rename(&rfp, &wfp, Filename, LocalBuffer); /* clean up and rename */ +} + +/* Ansi C "itoa" based on Kernighan & Ritchie's "Ansi C" book. */ +#define ABS(v) ((v) < 0 ? -(v) : (v)) + +static void strreverse(TCHAR *str) +{ + int i, j; + for (i = 0, j = (int)_tcslen(str) - 1; i < j; i++, j--) { + TCHAR t = str[i]; + str[i] = str[j]; + str[j] = t; + } +} + +static void long2str(long value, TCHAR *str) +{ + int i = 0; + long sign = value; + + /* generate digits in reverse order */ + do { + int n = (int)(value % 10); /* get next lowest digit */ + str[i++] = (TCHAR)(ABS(n) + '0'); /* handle case of negative digit */ + } while (value /= 10); /* delete the lowest digit */ + if (sign < 0) + str[i++] = '-'; + str[i] = '\0'; + + strreverse(str); +} + +/** ini_putl() + * \param Section the name of the section to write the value in + * \param Key the name of the entry to write + * \param Value the value to write + * \param Filename the name and full path of the .ini file to write to + * + * \return 1 if successful, otherwise 0 + */ +int ini_putl(const TCHAR *Section, const TCHAR *Key, long Value, const TCHAR *Filename) +{ + TCHAR LocalBuffer[32]; + long2str(Value, LocalBuffer); + return ini_puts(Section, Key, LocalBuffer, Filename); +} + +#if defined INI_REAL +/** ini_putf() + * \param Section the name of the section to write the value in + * \param Key the name of the entry to write + * \param Value the value to write + * \param Filename the name and full path of the .ini file to write to + * + * \return 1 if successful, otherwise 0 + */ +int ini_putf(const TCHAR *Section, const TCHAR *Key, INI_REAL Value, const TCHAR *Filename) +{ + TCHAR LocalBuffer[64]; + ini_ftoa(LocalBuffer, Value); + return ini_puts(Section, Key, LocalBuffer, Filename); +} +#endif /* INI_REAL */ +#endif /* !INI_READONLY */ diff --git a/common/minIni/minIni.h b/common/minIni/minIni.h new file mode 100644 index 0000000..b63451c --- /dev/null +++ b/common/minIni/minIni.h @@ -0,0 +1,166 @@ +/* minIni - Multi-Platform INI file parser, suitable for embedded systems + * + * Copyright (c) CompuPhase, 2008-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Version: $Id: minIni.h 53 2015-01-18 13:35:11Z thiadmer.riemersma@gmail.com $ + */ +#ifndef MININI_H +#define MININI_H + +#include "minGlue.h" + +#if (defined _UNICODE || defined __UNICODE__ || defined UNICODE) && !defined INI_ANSIONLY + #include + #define mTCHAR TCHAR +#else + /* force TCHAR to be "char", but only for minIni */ + #define mTCHAR char +#endif + +#if !defined INI_BUFFERSIZE + #define INI_BUFFERSIZE 512 +#endif + +#if defined __cplusplus + extern "C" { +#endif + +int ini_getbool(const mTCHAR *Section, const mTCHAR *Key, int DefValue, const mTCHAR *Filename); +long ini_getl(const mTCHAR *Section, const mTCHAR *Key, long DefValue, const mTCHAR *Filename); +int ini_gets(const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *DefValue, mTCHAR *Buffer, int BufferSize, const mTCHAR *Filename); +int ini_getsection(int idx, mTCHAR *Buffer, int BufferSize, const mTCHAR *Filename); +int ini_getkey(const mTCHAR *Section, int idx, mTCHAR *Buffer, int BufferSize, const mTCHAR *Filename); + +int ini_hassection(const mTCHAR *Section, const mTCHAR *Filename); +int ini_haskey(const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Filename); + +#if defined INI_REAL +INI_REAL ini_getf(const mTCHAR *Section, const mTCHAR *Key, INI_REAL DefValue, const mTCHAR *Filename); +#endif + +#if !defined INI_READONLY +int ini_putl(const mTCHAR *Section, const mTCHAR *Key, long Value, const mTCHAR *Filename); +int ini_puts(const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, const mTCHAR *Filename); +#if defined INI_REAL +int ini_putf(const mTCHAR *Section, const mTCHAR *Key, INI_REAL Value, const mTCHAR *Filename); +#endif +#endif /* INI_READONLY */ + +#if !defined INI_NOBROWSE +typedef int (*INI_CALLBACK)(const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData); +int ini_browse(INI_CALLBACK Callback, void *UserData, const mTCHAR *Filename); +#endif /* INI_NOBROWSE */ + +#if defined __cplusplus + } +#endif + + +#if defined __cplusplus + +#if defined __WXWINDOWS__ + #include "wxMinIni.h" +#else + #include + + /* The C++ class in minIni.h was contributed by Steven Van Ingelgem. */ + class minIni + { + public: + minIni(const std::string& filename) : iniFilename(filename) + { } + + bool getbool(const std::string& Section, const std::string& Key, bool DefValue=false) const + { return ini_getbool(Section.c_str(), Key.c_str(), int(DefValue), iniFilename.c_str()) != 0; } + + long getl(const std::string& Section, const std::string& Key, long DefValue=0) const + { return ini_getl(Section.c_str(), Key.c_str(), DefValue, iniFilename.c_str()); } + + int geti(const std::string& Section, const std::string& Key, int DefValue=0) const + { return static_cast(this->getl(Section, Key, long(DefValue))); } + + std::string gets(const std::string& Section, const std::string& Key, const std::string& DefValue="") const + { + char buffer[INI_BUFFERSIZE]; + ini_gets(Section.c_str(), Key.c_str(), DefValue.c_str(), buffer, INI_BUFFERSIZE, iniFilename.c_str()); + return buffer; + } + + std::string getsection(int idx) const + { + char buffer[INI_BUFFERSIZE]; + ini_getsection(idx, buffer, INI_BUFFERSIZE, iniFilename.c_str()); + return buffer; + } + + std::string getkey(const std::string& Section, int idx) const + { + char buffer[INI_BUFFERSIZE]; + ini_getkey(Section.c_str(), idx, buffer, INI_BUFFERSIZE, iniFilename.c_str()); + return buffer; + } + + bool hassection(const std::string& Section) const + { return ini_hassection(Section.c_str(), iniFilename.c_str()) != 0; } + + bool haskey(const std::string& Section, const std::string& Key) const + { return ini_haskey(Section.c_str(), Key.c_str(), iniFilename.c_str()) != 0; } + +#if defined INI_REAL + INI_REAL getf(const std::string& Section, const std::string& Key, INI_REAL DefValue=0) const + { return ini_getf(Section.c_str(), Key.c_str(), DefValue, iniFilename.c_str()); } +#endif + +#if ! defined INI_READONLY + bool put(const std::string& Section, const std::string& Key, long Value) + { return ini_putl(Section.c_str(), Key.c_str(), Value, iniFilename.c_str()) != 0; } + + bool put(const std::string& Section, const std::string& Key, int Value) + { return ini_putl(Section.c_str(), Key.c_str(), (long)Value, iniFilename.c_str()) != 0; } + + bool put(const std::string& Section, const std::string& Key, bool Value) + { return ini_putl(Section.c_str(), Key.c_str(), (long)Value, iniFilename.c_str()) != 0; } + + bool put(const std::string& Section, const std::string& Key, const std::string& Value) + { return ini_puts(Section.c_str(), Key.c_str(), Value.c_str(), iniFilename.c_str()) != 0; } + + bool put(const std::string& Section, const std::string& Key, const char* Value) + { return ini_puts(Section.c_str(), Key.c_str(), Value, iniFilename.c_str()) != 0; } + +#if defined INI_REAL + bool put(const std::string& Section, const std::string& Key, INI_REAL Value) + { return ini_putf(Section.c_str(), Key.c_str(), Value, iniFilename.c_str()) != 0; } +#endif + + bool del(const std::string& Section, const std::string& Key) + { return ini_puts(Section.c_str(), Key.c_str(), 0, iniFilename.c_str()) != 0; } + + bool del(const std::string& Section) + { return ini_puts(Section.c_str(), 0, 0, iniFilename.c_str()) != 0; } +#endif + +#if !defined INI_NOBROWSE + bool browse(INI_CALLBACK Callback, void *UserData) const + { return ini_browse(Callback, UserData, iniFilename.c_str()) != 0; } +#endif + + private: + std::string iniFilename; + }; + +#endif /* __WXWINDOWS__ */ +#endif /* __cplusplus */ + +#endif /* MININI_H */ diff --git a/common/pm/pm.cpp b/common/pm/pm.cpp new file mode 100644 index 0000000..e1021eb --- /dev/null +++ b/common/pm/pm.cpp @@ -0,0 +1,51 @@ +#include "pm.hpp" + +namespace { + +constexpr u64 QLAUNCH_TITLE_ID{0x0100000000001000ULL}; +u64 CURRENT_TITLE_ID{}; + +} + +namespace pm { + +auto Initialize() -> Result { + auto rc = pmdmntInitialize(); + if (R_FAILED(rc)) { + return rc; + } + + return pminfoInitialize(); +} + +void Exit() { + pminfoExit(); + pmdmntExit(); +} + +// SOURCE: https://github.com/retronx-team/sys-clk/blob/570f1e5fe10b253eff0c8fda1bb893bb620af052/sysmodule/src/process_management.cpp#L37 +void getCurrentPidTid(u64* pid_out, u64* tid_out) { + Result rc{}; + if (R_SUCCEEDED(rc = pmdmntGetApplicationProcessId(pid_out))) { + if (0x20f == pminfoGetProgramId(tid_out, *pid_out)){ + *tid_out = QLAUNCH_TITLE_ID; + } + } else if (rc == 0x20f) { + *tid_out = QLAUNCH_TITLE_ID; + } else { + *tid_out = CURRENT_TITLE_ID; + } +} + +auto PollCurrentPidTid(u64* pid_out, u64* tid_out) -> bool { + getCurrentPidTid(pid_out, tid_out); + + if (*tid_out != CURRENT_TITLE_ID) { + CURRENT_TITLE_ID = *tid_out; + return true; + } + + return false; +} + +} diff --git a/common/pm/pm.hpp b/common/pm/pm.hpp new file mode 100644 index 0000000..684744c --- /dev/null +++ b/common/pm/pm.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace pm { + +auto Initialize() -> Result; +void Exit(); +void getCurrentPidTid(u64* pid_out, u64* tid_out); +auto PollCurrentPidTid(u64* pid_out, u64* tid_out) -> bool; + +} diff --git a/sys-tune/source/impl/sdmc.cpp b/common/sdmc/sdmc.cpp similarity index 71% rename from sys-tune/source/impl/sdmc.cpp rename to common/sdmc/sdmc.cpp index 95d8b6c..9b91a46 100644 --- a/sys-tune/source/impl/sdmc.cpp +++ b/common/sdmc/sdmc.cpp @@ -27,7 +27,12 @@ namespace sdmc { bool FileExists(const char* path) { std::strcpy(path_buffer, path); FsTimeStampRaw ts; - return R_SUCCEEDED(fsFsGetFileTimeStampRaw(&sdmc, path, &ts)); + return R_SUCCEEDED(fsFsGetFileTimeStampRaw(&sdmc, path_buffer, &ts)); + } + + Result CreateFolder(const char* path) { + std::strcpy(path_buffer, path); + return fsFsCreateDirectory(&sdmc, path_buffer); } } diff --git a/sys-tune/source/impl/sdmc.hpp b/common/sdmc/sdmc.hpp similarity index 83% rename from sys-tune/source/impl/sdmc.hpp rename to common/sdmc/sdmc.hpp index a31f6fb..11a6575 100644 --- a/sys-tune/source/impl/sdmc.hpp +++ b/common/sdmc/sdmc.hpp @@ -10,4 +10,6 @@ namespace sdmc { Result OpenFile(FsFile *file, const char* path, int open_mode = FsOpenMode_Read); bool FileExists(const char* path); + Result CreateFolder(const char* path); + } diff --git a/ipc/ipc_cmd.h b/ipc/ipc_cmd.h index 2bac9c7..4901b46 100644 --- a/ipc/ipc_cmd.h +++ b/ipc/ipc_cmd.h @@ -9,6 +9,10 @@ enum TuneIpcCmd { TuneIpcCmd_GetVolume = 10, TuneIpcCmd_SetVolume = 11, + TuneIpcCmd_GetTitleVolume = 12, + TuneIpcCmd_SetTitleVolume = 13, + TuneIpcCmd_GetDefaultTitleVolume = 14, + TuneIpcCmd_SetDefaultTitleVolume = 15, TuneIpcCmd_GetRepeatMode = 20, TuneIpcCmd_SetRepeatMode = 21, @@ -29,4 +33,4 @@ enum TuneIpcCmd { TuneIpcCmd_QuitServer = 50, TuneIpcCmd_GetApiVersion = 5000, -}; \ No newline at end of file +}; diff --git a/ipc/tune.c b/ipc/tune.c index 98710f9..4b3dd28 100644 --- a/ipc/tune.c +++ b/ipc/tune.c @@ -49,6 +49,22 @@ Result tuneSetVolume(float volume) { return serviceDispatchIn(&g_tune, TuneIpcCmd_SetVolume, volume); } +Result tuneGetTitleVolume(float *out) { + return serviceDispatchOut(&g_tune, TuneIpcCmd_GetTitleVolume, *out); +} + +Result tuneSetTitleVolume(float volume) { + return serviceDispatchIn(&g_tune, TuneIpcCmd_SetTitleVolume, volume); +} + +Result tuneGetDefaultTitleVolume(float *out) { + return serviceDispatchOut(&g_tune, TuneIpcCmd_GetDefaultTitleVolume, *out); +} + +Result tuneSetDefaultTitleVolume(float volume) { + return serviceDispatchIn(&g_tune, TuneIpcCmd_SetDefaultTitleVolume, volume); +} + Result tuneGetRepeatMode(TuneRepeatMode *state) { u8 out = 0; Result rc = serviceDispatchOut(&g_tune, TuneIpcCmd_GetRepeatMode, out); diff --git a/ipc/tune.h b/ipc/tune.h index ea7dee5..acd6d88 100644 --- a/ipc/tune.h +++ b/ipc/tune.h @@ -63,6 +63,30 @@ Result tuneGetVolume(float *out); */ Result tuneSetVolume(float volume); +/** + * @brief Get the volume of the current title + * @param[out] out volume value (linear factor). + */ +Result tuneGetTitleVolume(float *out); + +/** + * @brief Set the volume of the current title + * @param[in] volume volume value (linear factor). + */ +Result tuneSetTitleVolume(float volume); + +/** + * @brief Get the default volume of all titles + * @param[out] out volume value (linear factor). + */ +Result tuneGetDefaultTitleVolume(float *out); + +/** + * @brief Set the default volume of all titles + * @param[in] volume volume value (linear factor). + */ +Result tuneSetDefaultTitleVolume(float volume); + /** * @brief Get the current loop status. * @param[out] state \ref TuneRepeatMode diff --git a/overlay/Makefile b/overlay/Makefile index 79575bd..b797f9f 100644 --- a/overlay/Makefile +++ b/overlay/Makefile @@ -37,13 +37,13 @@ include $(DEVKITPRO)/libnx/switch_rules # of a homebrew executable (.nro). This is intended to be used for sysmodules. # NACP building is skipped as well. #--------------------------------------------------------------------------------- -APP_TITLE := sys-tune overlay +APP_TITLE := sys-tune TARGET := sys-tune-overlay BUILD := build -SOURCES := source ../ipc +SOURCES := source ../ipc ../common/minIni ../common/sdmc ../common/config ../common/pm ../common/aud DATA := data -INCLUDES := lib/include ../ipc +INCLUDES := lib/include ../ipc ../common APP_VERSION := $(VERSION) NO_ICON := 1 @@ -70,13 +70,13 @@ endif #--------------------------------------------------------------------------------- ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE -CFLAGS := -g -Wall -Wno-format-truncation -O3 -ffunction-sections \ +CFLAGS := -g -Wall -Wno-format-truncation -O2 -ffunction-sections \ $(ARCH) $(DEFINES) CFLAGS += $(INCLUDE) -DVERSION=\"v$(APP_VERSION)\" -DTUNE_API_VERSION=$(API_VERSION) \ $(WANT_FLAGS) -CXXFLAGS := $(CFLAGS) -fno-exceptions -std=c++20 +CXXFLAGS := $(CFLAGS) -fno-exceptions -std=c++23 ASFLAGS := -g $(ARCH) LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) @@ -203,7 +203,7 @@ DEPENDS := $(OFILES:.o=.d) #--------------------------------------------------------------------------------- all : $(OUTPUT).ovl -$(OUTPUT).ovl : $(OUTPUT).elf $(OUTPUT).nacp +$(OUTPUT).ovl : $(OUTPUT).elf $(OUTPUT).nacp @elf2nro $< $@ $(NROFLAGS) @echo "built ... $(notdir $(OUTPUT).ovl)" diff --git a/overlay/lib b/overlay/lib index 779b4ea..f766e9b 160000 --- a/overlay/lib +++ b/overlay/lib @@ -1 +1 @@ -Subproject commit 779b4ead7df6b277b947a535544aa519785c437e +Subproject commit f766e9b607a05e9756843cbd62b3bfb98be1646c diff --git a/overlay/source/elm_overlayframe.hpp b/overlay/source/elm_overlayframe.hpp index 2015566..2985a60 100644 --- a/overlay/source/elm_overlayframe.hpp +++ b/overlay/source/elm_overlayframe.hpp @@ -8,7 +8,7 @@ * @brief The base frame which can contain another view * */ -class SysTuneOverlayFrame : public tsl::elm::Element { +class SysTuneOverlayFrame final : public tsl::elm::Element { public: /** * @brief Constructor @@ -17,9 +17,8 @@ class SysTuneOverlayFrame : public tsl::elm::Element { * @param subtitle Subtitle drawn bellow the title e.g version number */ SysTuneOverlayFrame() : tsl::elm::Element() {} - virtual ~SysTuneOverlayFrame() {} - virtual void draw(tsl::gfx::Renderer *renderer) override { + void draw(tsl::gfx::Renderer *renderer) override { renderer->fillScreen(a(tsl::style::color::ColorFrameBackground)); renderer->drawRect(tsl::cfg::FramebufferWidth - 1, 0, 1, tsl::cfg::FramebufferHeight, a(0xF222)); @@ -38,7 +37,7 @@ class SysTuneOverlayFrame : public tsl::elm::Element { s32 height = 110; s32 startX = 10; s32 startY = tsl::cfg::FramebufferHeight - height; - + u32 fadeDuration = 10; if (m_toast->Current < fadeDuration) { s32 offset = height - (height / fadeDuration) * m_toast->Current; @@ -59,7 +58,7 @@ class SysTuneOverlayFrame : public tsl::elm::Element { } } - virtual void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override { + void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override { setBoundaries(parentX, parentY, parentWidth, parentHeight); if (m_contentElement != nullptr) { @@ -68,14 +67,14 @@ class SysTuneOverlayFrame : public tsl::elm::Element { } } - virtual Element* requestFocus(tsl::elm::Element *oldFocus, tsl::FocusDirection direction) override { + Element* requestFocus(tsl::elm::Element *oldFocus, tsl::FocusDirection direction) override { if (m_contentElement != nullptr) return m_contentElement->requestFocus(oldFocus, direction); else return nullptr; } - virtual bool onTouch(tsl::elm::TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) override { + bool onTouch(tsl::elm::TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) override { // Discard touches outside bounds if (!m_contentElement->inBounds(currX, currY)) return false; @@ -117,6 +116,6 @@ class SysTuneOverlayFrame : public tsl::elm::Element { private: std::unique_ptr m_contentElement; const char *m_description = "\uE0E1 Back \uE0E0 OK"; - + std::optional m_toast; }; diff --git a/overlay/source/elm_status_bar.cpp b/overlay/source/elm_status_bar.cpp index 87a3415..b08db54 100644 --- a/overlay/source/elm_status_bar.cpp +++ b/overlay/source/elm_status_bar.cpp @@ -1,6 +1,7 @@ #include "elm_status_bar.hpp" #include "symbol.hpp" +#include "config/config.hpp" namespace { @@ -242,11 +243,13 @@ void StatusBar::update() { void StatusBar::CycleRepeat() { this->m_repeat = static_cast((this->m_repeat + 1) % TuneRepeatMode_Count); + config::set_repeat(this->m_repeat); tuneSetRepeatMode(this->m_repeat); } void StatusBar::CycleShuffle() { this->m_shuffle = static_cast((this->m_shuffle + 1) % TuneShuffleMode_Count); + config::set_shuffle(this->m_shuffle); tuneSetShuffleMode(this->m_shuffle); } diff --git a/overlay/source/elm_status_bar.hpp b/overlay/source/elm_status_bar.hpp index 6108097..8acef0d 100644 --- a/overlay/source/elm_status_bar.hpp +++ b/overlay/source/elm_status_bar.hpp @@ -5,7 +5,7 @@ #include -class StatusBar : public tsl::elm::Element { +class StatusBar final : public tsl::elm::Element { private: bool m_playing; TuneRepeatMode m_repeat; @@ -26,11 +26,11 @@ class StatusBar : public tsl::elm::Element { public: StatusBar(); - virtual tsl::elm::Element *requestFocus(tsl::elm::Element *oldFocus, tsl::FocusDirection direction) final; - virtual void draw(tsl::gfx::Renderer *renderer) final; - virtual void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) final; - virtual bool onClick(u64 keys) final; - virtual bool onTouch(tsl::elm::TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) final; + tsl::elm::Element *requestFocus(tsl::elm::Element *oldFocus, tsl::FocusDirection direction) override; + void draw(tsl::gfx::Renderer *renderer) override; + void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override; + bool onClick(u64 keys) override; + bool onTouch(tsl::elm::TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) override; void update(); diff --git a/overlay/source/elm_volume.hpp b/overlay/source/elm_volume.hpp new file mode 100644 index 0000000..c8da92b --- /dev/null +++ b/overlay/source/elm_volume.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "tesla.hpp" + + +class ElmVolume final : public tsl::elm::StepTrackBar { +public: + ElmVolume(const char icon[3], const std::string& name, size_t numSteps) + : StepTrackBar{icon, numSteps}, m_name{name} { } + + virtual ~ElmVolume() {} + + virtual void draw(tsl::gfx::Renderer *renderer) override { + const u16 trackBarWidth = this->getWidth() - 95; + const u16 stepWidth = trackBarWidth / (this->m_numSteps - 1); + + for (u8 i = 0; i < this->m_numSteps; i++) { + renderer->drawRect(this->getX() + 60 + stepWidth * i, this->getY() + 50, 1, 10, a(tsl::style::color::ColorFrame)); + } + + const auto [descWidth, descHeight] = renderer->drawString(this->m_name.c_str(), false, 0, 0, 15, tsl::style::color::ColorTransparent); + renderer->drawString(this->m_name.c_str(), false, ((this->getX() + 60) + (this->getWidth() - 95) / 2) - (descWidth / 2), this->getY() + 20, 15, a(tsl::style::color::ColorDescription)); + + StepTrackBar::draw(renderer); + } + +private: + const std::string m_name; +}; diff --git a/overlay/source/gui_browser.cpp b/overlay/source/gui_browser.cpp index eefa2f1..5e99a95 100644 --- a/overlay/source/gui_browser.cpp +++ b/overlay/source/gui_browser.cpp @@ -36,10 +36,12 @@ namespace { return false; } + constexpr const char *const base_path = "/music/"; + + char path_buffer[FS_MAX_PATH]; + } -constexpr const char *const base_path = "/music/"; -char path_buffer[FS_MAX_PATH]; BrowserGui::BrowserGui() : m_fs(), has_music(), cwd("/") { @@ -76,8 +78,6 @@ tsl::elm::Element *BrowserGui::createUI() { return m_frame; } -void BrowserGui::update() { -} bool BrowserGui::handleInput(u64 keysDown, u64, const HidTouchState&, HidAnalogStickState, HidAnalogStickState) { if (keysDown & HidNpadButton_B) { if (this->has_music && this->cwd[7] == '\0') { @@ -120,7 +120,7 @@ void BrowserGui::scanCwd() { while (R_SUCCEEDED(fsDirRead(&dir, &count, 1, &entry)) && count) { if (entry.type == FsDirEntryType_Dir) { /* Add directory entries. */ - auto *item = new tsl::elm::ListItem(entry.name); + auto item = new tsl::elm::ListItem(entry.name); item->setClickListener([this, item](u64 down) -> bool { if (down & HidNpadButton_A) { std::strncat(this->cwd, item->getText().c_str(), sizeof(this->cwd) - 1); @@ -133,7 +133,7 @@ void BrowserGui::scanCwd() { folders.push_back(item); } else if (SupportsType(entry.name)) { /* Add file entry. */ - auto *item = new tsl::elm::ListItem(entry.name); + auto item = new tsl::elm::ListItem(entry.name); item->setClickListener([this, item](u64 down) -> bool { if (down & HidNpadButton_A) { std::snprintf(path_buffer, sizeof(path_buffer), "%s%s", this->cwd, item->getText().c_str()); @@ -193,8 +193,8 @@ void BrowserGui::addAllToPlaylist() { return; } tsl::hlp::ScopeGuard dirGuard([&] { fsDirClose(&dir); }); - - std::vector file_list; + + std::vector file_list; s64 songs_added = 0; s64 count = 0; FsDirectoryEntry entry; diff --git a/overlay/source/gui_browser.hpp b/overlay/source/gui_browser.hpp index dec9f3c..ae06e01 100644 --- a/overlay/source/gui_browser.hpp +++ b/overlay/source/gui_browser.hpp @@ -16,9 +16,8 @@ class BrowserGui final : public tsl::Gui { BrowserGui(); ~BrowserGui(); - virtual tsl::elm::Element *createUI() final; - virtual void update() final; - virtual bool handleInput(u64 keysDown, u64, const HidTouchState&, HidAnalogStickState, HidAnalogStickState) final; + tsl::elm::Element *createUI() override; + bool handleInput(u64 keysDown, u64, const HidTouchState&, HidAnalogStickState, HidAnalogStickState) override; private: void scanCwd(); diff --git a/overlay/source/gui_error.cpp b/overlay/source/gui_error.cpp index 928d26c..066d201 100644 --- a/overlay/source/gui_error.cpp +++ b/overlay/source/gui_error.cpp @@ -2,7 +2,11 @@ #include "elm_overlayframe.hpp" -static char result_buffer[10]; +namespace { + + char result_buffer[10]; + +} ErrorGui::ErrorGui(const char *msg, Result rc) : m_msg(msg) { if (rc) { @@ -14,7 +18,7 @@ ErrorGui::ErrorGui(const char *msg, Result rc) : m_msg(msg) { tsl::elm::Element *ErrorGui::createUI() { auto rootFrame = new SysTuneOverlayFrame(); - auto *custom = new tsl::elm::CustomDrawer([this](tsl::gfx::Renderer *drawer, u16 x, u16 y, u16 w, u16 h) { + auto custom = new tsl::elm::CustomDrawer([this](tsl::gfx::Renderer *drawer, u16 x, u16 y, u16 w, u16 h) { drawer->drawString("\uE150", false, x + (w / 2) - (90 / 2), 300, 90, 0xffff); auto [width, height] = drawer->drawString(this->m_msg, false, x + (w / 2) - (this->msgW / 2), 380, 25, 0xffff); if (msgW == 0) { diff --git a/overlay/source/gui_error.hpp b/overlay/source/gui_error.hpp index 59bf0b7..721f51e 100644 --- a/overlay/source/gui_error.hpp +++ b/overlay/source/gui_error.hpp @@ -11,5 +11,5 @@ class ErrorGui final : public tsl::Gui { public: ErrorGui(const char *msg, Result rc); - virtual tsl::elm::Element *createUI() final; + tsl::elm::Element *createUI() override; }; diff --git a/overlay/source/gui_main.cpp b/overlay/source/gui_main.cpp index 85ca819..8409ea9 100644 --- a/overlay/source/gui_main.cpp +++ b/overlay/source/gui_main.cpp @@ -1,43 +1,32 @@ #include "gui_main.hpp" #include "elm_overlayframe.hpp" +#include "elm_volume.hpp" #include "gui_browser.hpp" #include "gui_playlist.hpp" +#include "pm/pm.hpp" +#include "config/config.hpp" namespace { - constexpr const size_t num_steps = 20; - constexpr const float max_volume = 2; - - void volumeCallback(u8 value) { - float music_volume = (float(value) / num_steps) * max_volume; - tuneSetVolume(music_volume); - } - } MainGui::MainGui() { m_status_bar = new StatusBar(); - m_volume_slider = new tsl::elm::StepTrackBar("\uE13C", num_steps); - /* Get initial volume. */ - float volume = 0; - if (R_SUCCEEDED(tuneGetVolume(&volume))) { - this->m_volume_slider->setProgress((volume / max_volume) * num_steps); - this->m_volume_slider->setValueChangedListener(volumeCallback); - } else { - this->m_volume_slider->setProgress(0); - } } tsl::elm::Element *MainGui::createUI() { - auto *frame = new SysTuneOverlayFrame(); - auto *list = new tsl::elm::List(); + auto frame = new SysTuneOverlayFrame(); + auto list = new tsl::elm::List(); + + u64 pid{}, tid{}; + pm::getCurrentPidTid(&pid, &tid); /* Current track. */ list->addItem(this->m_status_bar, tsl::style::ListItemDefaultHeight * 2); /* Playlist. */ - auto *queue_button = new tsl::elm::ListItem("Playlist"); + auto queue_button = new tsl::elm::ListItem("Playlist"); queue_button->setClickListener([](u64 keys) { if (keys & HidNpadButton_A) { tsl::changeTo(); @@ -48,7 +37,7 @@ tsl::elm::Element *MainGui::createUI() { list->addItem(queue_button); /* Browser. */ - auto *browser_button = new tsl::elm::ListItem("Music browser"); + auto browser_button = new tsl::elm::ListItem("Music browser"); browser_button->setClickListener([](u64 keys) { if (keys & HidNpadButton_A) { tsl::changeTo(); @@ -59,9 +48,74 @@ tsl::elm::Element *MainGui::createUI() { list->addItem(browser_button); /* Volume indicator */ - list->addItem(this->m_volume_slider); + list->addItem(new tsl::elm::CategoryHeader("Volume Control")); + + /* Get initial volume. */ + float tune_volume = 1.f; + float title_volume = 1.f; + float default_title_volume = 1.f; + + tuneGetVolume(&tune_volume); + tuneGetTitleVolume(&title_volume); + tuneGetDefaultTitleVolume(&default_title_volume); + + auto tune_volume_slider = new ElmVolume("\uE13C", "Tune Volume", num_steps); + tune_volume_slider->setProgress(tune_volume * num_steps); + tune_volume_slider->setValueChangedListener([](u8 value){ + const float volume = float(value) / float(num_steps); + tuneSetVolume(volume); + }); + list->addItem(tune_volume_slider); + + // empty pid means we are qlaunch :) + if (tid && pid) { + auto title_volume_slider = new ElmVolume("\uE13C", "Game Volume", num_steps); + title_volume_slider->setProgress(title_volume * num_steps); + title_volume_slider->setValueChangedListener([tid](u8 value){ + const float volume = float(value) / float(num_steps); + tuneSetTitleVolume(volume); + config::set_title_volume(tid, volume); + }); + list->addItem(title_volume_slider); + } + + auto default_title_volume_slider = new ElmVolume("\uE13C", "Game Volume (default)", num_steps); + default_title_volume_slider->setProgress(default_title_volume * num_steps); + default_title_volume_slider->setValueChangedListener([](u8 value){ + const float volume = float(value) / float(num_steps); + tuneSetDefaultTitleVolume(volume); + }); + list->addItem(default_title_volume_slider); + + list->addItem(new tsl::elm::CategoryHeader("Play / Pause")); + + /* Per title tune toggle. */ + auto tune_play = new tsl::elm::ToggleListItem("Tune", config::get_title_enabled(tid), "Play", "Pause"); + tune_play->setStateChangedListener([tid](bool new_value) { + config::set_title_enabled(tid, new_value); + if (new_value) { + tunePlay(); + } else { + tunePause(); + } + }); + list->addItem(tune_play); + + /* Default title tune toggle. */ + auto tune_default_play = new tsl::elm::ToggleListItem("Tune (default)", config::get_title_enabled_default(), "Play", "Pause"); + tune_default_play->setStateChangedListener([](bool new_value) { + config::set_title_enabled_default(new_value); + if (new_value) { + tunePlay(); + } else { + tunePause(); + } + }); + list->addItem(tune_default_play); + + list->addItem(new tsl::elm::CategoryHeader("Misc")); - auto *exit_button = new tsl::elm::ListItem("Close sys-tune"); + auto exit_button = new tsl::elm::ListItem("Close sys-tune"); exit_button->setClickListener([](u64 keys) { if (keys & HidNpadButton_A) { tuneQuit(); diff --git a/overlay/source/gui_main.hpp b/overlay/source/gui_main.hpp index cb91a92..b2ab01a 100644 --- a/overlay/source/gui_main.hpp +++ b/overlay/source/gui_main.hpp @@ -8,12 +8,11 @@ class MainGui final : public tsl::Gui { private: StatusBar *m_status_bar; - tsl::elm::TrackBar *m_volume_slider; public: MainGui(); - virtual tsl::elm::Element *createUI() final; + tsl::elm::Element *createUI() final; - virtual void update() final; + void update() final; }; diff --git a/overlay/source/gui_playlist.cpp b/overlay/source/gui_playlist.cpp index 98d1736..f701ff8 100644 --- a/overlay/source/gui_playlist.cpp +++ b/overlay/source/gui_playlist.cpp @@ -16,12 +16,12 @@ namespace { } } - class ButtonListItem : public tsl::elm::ListItem { + class ButtonListItem final : public tsl::elm::ListItem { public: template ButtonListItem(Text &text, Value &value) : ListItem(std::forward(text), std::forward(value)) {} - virtual bool onTouch(tsl::elm::TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) override { + bool onTouch(tsl::elm::TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) override { if (event == tsl::elm::TouchEvent::Touch) this->m_touched = this->inBounds(currX, currY); @@ -80,7 +80,7 @@ PlaylistGui::PlaylistGui() { break; } } - auto *item = new ButtonListItem(str, "\uE098"); + auto item = new ButtonListItem(str, "\uE098"); item->setClickListener([this, item](u64 keys) -> bool { u32 index = this->m_list->getIndexInList(item); u8 counter = 0; @@ -92,7 +92,7 @@ PlaylistGui::PlaylistGui() { if (R_SUCCEEDED(tuneRemove(index))) { this->removeFocus(); this->m_list->removeIndex(index); - auto *element = this->m_list->getItemAtIndex(index + 1); + auto element = this->m_list->getItemAtIndex(index + 1); if (element != nullptr) { this->requestFocus(element, tsl::FocusDirection::Down); this->m_list->setFocusedIndex(index + 1); diff --git a/overlay/source/gui_playlist.hpp b/overlay/source/gui_playlist.hpp index 2ed90ae..781c819 100644 --- a/overlay/source/gui_playlist.hpp +++ b/overlay/source/gui_playlist.hpp @@ -9,5 +9,5 @@ class PlaylistGui final : public tsl::Gui { public: PlaylistGui(); - virtual tsl::elm::Element *createUI() final; + tsl::elm::Element *createUI() override; }; diff --git a/overlay/source/main.cpp b/overlay/source/main.cpp index 3719382..97aee56 100644 --- a/overlay/source/main.cpp +++ b/overlay/source/main.cpp @@ -2,35 +2,57 @@ #include "tune.h" #include "gui_error.hpp" #include "gui_main.hpp" +#include "sdmc/sdmc.hpp" +#include "pm/pm.hpp" +#include "config/config.hpp" #include -class OverlayTest : public tsl::Overlay { +class SysTuneOverlay final : public tsl::Overlay { private: const char *msg = nullptr; Result fail = 0; public: - virtual void initServices() override { + void initServices() override { + if (R_FAILED(pm::Initialize())) { + this->msg = "Failed pm::Initialize()"; + return; + } + + // don't open sys-tune if blacklisted title is active! + u64 pid{}, tid{}; + pm::getCurrentPidTid(&pid, &tid); + + if (config::get_title_blacklist(tid)) { + this->msg = + "Title is blacklisted!\n" + "Exit to use sys-tune"; + return; + } + Result rc = tuneInitialize(); - if (R_VALUE(rc) == KERNELRESULT(NotFound)) { + // not found can happen if the service isn't started + // connection refused can happen is the service was terminated by pmshell + if (R_VALUE(rc) == KERNELRESULT(NotFound) || KERNELRESULT(ConnectionRefused)) { u64 pid = 0; const NcmProgramLocation programLocation{ .program_id = 0x4200000000000000, .storageID = NcmStorageId_None, }; rc = pmshellInitialize(); - if (R_SUCCEEDED(rc)) + if (R_SUCCEEDED(rc)) { rc = pmshellLaunchProgram(0, &programLocation, &pid); - pmshellExit(); + pmshellExit(); + } if (R_FAILED(rc) || pid == 0) { this->fail = rc; this->msg = " Failed to\n" "launch sysmodule"; return; } - svcSleepThread(100'000'000); + svcSleepThread(500'000'000ULL); rc = tuneInitialize(); } @@ -40,20 +62,25 @@ class OverlayTest : public tsl::Overlay { return; } + if (R_FAILED(sdmc::Open())) { + this->msg = "Failed sdmc::Open()"; + return; + } + u32 api; if (R_FAILED(tuneGetApiVersion(&api)) || api != TUNE_API_VERSION) { this->msg = " Unsupported\n" "sys-tune version!"; } } - virtual void exitServices() override { + + void exitServices() override { + sdmc::Close(); + pm::Exit(); tuneExit(); } - virtual void onShow() override {} - virtual void onHide() override {} - - virtual std::unique_ptr loadInitialGui() override { + std::unique_ptr loadInitialGui() override { if (this->msg) { return std::make_unique(this->msg, this->fail); } else { @@ -63,5 +90,5 @@ class OverlayTest : public tsl::Overlay { }; int main(int argc, char **argv) { - return tsl::loop(argc, argv); + return tsl::loop(argc, argv); } diff --git a/sys-tune/Makefile b/sys-tune/Makefile index 3e942d1..0d461e5 100644 --- a/sys-tune/Makefile +++ b/sys-tune/Makefile @@ -39,9 +39,9 @@ include $(DEVKITPRO)/libnx/switch_rules #--------------------------------------------------------------------------------- TARGET := $(notdir $(CURDIR)) BUILD := build -SOURCES := source source/impl +SOURCES := source source/impl ../common/minIni ../common/sdmc ../common/config ../common/pm ../common/aud DATA := data -INCLUDES := ../ipc +INCLUDES := ../ipc ../common WANT_FLAGS := @@ -68,7 +68,7 @@ CFLAGS := -g -Wall -O2 -ffunction-sections \ CFLAGS += $(INCLUDE) -DTUNE_API_VERSION=$(API_VERSION) \ $(WANT_FLAGS) -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++20 +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++23 ASFLAGS := -g $(ARCH) LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) diff --git a/sys-tune/nxExt/src/ipc_server.c b/sys-tune/nxExt/src/ipc_server.c index 1246e49..75d29b9 100644 --- a/sys-tune/nxExt/src/ipc_server.c +++ b/sys-tune/nxExt/src/ipc_server.c @@ -23,6 +23,16 @@ Result ipcServerInit(IpcServer* server, const char* name, u32 max_sessions) server->count = 0; Result rc = svcManageNamedPort(&server->handles[0], server->srvName.name, max_sessions); + if (R_FAILED(rc)) + { + rc = svcManageNamedPort(&server->handles[0], server->srvName.name, 0); + if(R_SUCCEEDED(rc)) + { + svcCloseHandle(server->handles[0]); + rc = svcManageNamedPort(&server->handles[0], server->srvName.name, max_sessions); + } + } + if(R_SUCCEEDED(rc)) { server->count = 1; @@ -201,4 +211,3 @@ Result ipcServerProcess(IpcServer* server, IpcServerRequestHandler handler, void return rc; } - diff --git a/sys-tune/source/impl/aud.c b/sys-tune/source/impl/aud.c new file mode 100644 index 0000000..3f0f3f4 --- /dev/null +++ b/sys-tune/source/impl/aud.c @@ -0,0 +1,113 @@ +#define NX_SERVICE_ASSUME_NON_DOMAIN +#include "aud.h" +#include + +static Service g_audaSrv; +static Service g_auddSrv; + +// IAudioSystemManagerForApplet +Result audaInitialize(void) { + if (hosversionBefore(11,0,0)) + return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer); + + return smGetService(&g_audaSrv, "aud:a"); +} + +void audaExit(void) { + serviceClose(&g_audaSrv); +} + +Service* audaGetServiceSession(void) { + return &g_audaSrv; +} + +Result audaRequestSuspendAudio(u64 pid, u64 delay) { + const struct { + u64 pid; + u64 timespan; + } in = { pid, delay }; + + return serviceDispatchIn(&g_audaSrv, 2, in); +} + +Result audaRequestResumeAudio(u64 pid, u64 delay) { + const struct { + u64 pid; + u64 timespan; + } in = { pid, delay }; + + return serviceDispatchIn(&g_audaSrv, 3, in); +} + +Result audaGetAudioOutputProcessMasterVolume(u64 pid, float* volume_out) { + return serviceDispatchInOut(&g_audaSrv, 4, pid, *volume_out); +} + +Result audaSetAudioOutputProcessMasterVolume(u64 pid, u64 delay, float volume) { + const struct { + float volume; + u64 pid; + u64 timespan; + } in = { volume, pid, delay }; + + return serviceDispatchIn(&g_audaSrv, 5, in); +} + +Result audaGetAudioInputProcessMasterVolume(u64 pid, float* volume_out) { + return serviceDispatchInOut(&g_audaSrv, 6, pid, *volume_out); +} + +Result audaSetAudioInputProcessMasterVolume(u64 pid, u64 delay, float volume) { + const struct { + float volume; + u64 pid; + u64 timespan; + } in = { volume, pid, delay }; + + return serviceDispatchIn(&g_audaSrv, 7, in); +} + +Result audaGetAudioOutputProcessRecordVolume(u64 pid, float* volume_out) { + return serviceDispatchInOut(&g_audaSrv, 8, pid, *volume_out); +} + +Result audaSetAudioOutputProcessRecordVolume(u64 pid, u64 delay, float volume) { + const struct { + float volume; + u64 pid; + u64 timespan; + } in = { volume, pid, delay }; + + return serviceDispatchIn(&g_audaSrv, 9, in); +} + +// IAudioSystemManagerForDebugger +Result auddInitialize(void) { + return smGetService(&g_auddSrv, "aud:d"); +} + +void auddExit(void) { + serviceClose(&g_auddSrv); +} + +Service* auddGetServiceSession(void) { + return &g_auddSrv; +} + +Result auddRequestSuspendAudioForDebug(u64 pid, u64 delay) { + const struct { + u64 pid; + u64 timespan; + } in = { pid, delay }; + + return serviceDispatchIn(&g_auddSrv, 0, in); +} + +Result auddRequestResumeAudioForDebug(u64 pid, u64 delay) { + const struct { + u64 pid; + u64 timespan; + } in = { pid, delay }; + + return serviceDispatchIn(&g_auddSrv, 1, in); +} diff --git a/sys-tune/source/impl/aud.h b/sys-tune/source/impl/aud.h new file mode 100644 index 0000000..f6d1832 --- /dev/null +++ b/sys-tune/source/impl/aud.h @@ -0,0 +1,51 @@ +/** + * @file aud.h + * @brief Only available on [11.0.0+]. + * @note Only one session may be open at once. + * @author TotalJustice + * @copyright libnx Authors + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define AUD_MAX_DELAY (1000000000ULL) + +/// Initialize aud:a. Only available on [11.0.0+]. +Result audaInitialize(void); +/// Exit aud:a. +void audaExit(void); +/// Gets the Service for aud:a. +Service* audaGetServiceSession(void); + +/// Initialize aud:d. Only available on [11.0.0+]. +Result auddInitialize(void); +/// Exit aud:d. +void auddExit(void); +/// Gets the Service for aud:d. +Service* auddGetServiceSession(void); + +// All tested (and works) +Result audaRequestSuspendAudio(u64 pid, u64 delay); +Result audaRequestResumeAudio(u64 pid, u64 delay); +Result audaGetAudioOutputProcessMasterVolume(u64 pid, float* volume_out); +// Doesn't seem to apply... Maybe Input is overriding this? +Result audaSetAudioOutputProcessMasterVolume(u64 pid, u64 delay, float volume); +Result audaGetAudioInputProcessMasterVolume(u64 pid, float* volume_out); +// Sets both Output and Input volume +Result audaSetAudioInputProcessMasterVolume(u64 pid, u64 delay, float volume); +Result audaGetAudioOutputProcessRecordVolume(u64 pid, float* volume_out); +Result audaSetAudioOutputProcessRecordVolume(u64 pid, u64 delay, float volume); + +// All tested (and works) +Result auddRequestSuspendAudioForDebug(u64 pid, u64 delay); +Result auddRequestResumeAudioForDebug(u64 pid, u64 delay); + +#ifdef __cplusplus +} +#endif diff --git a/sys-tune/source/impl/aud_wrapper.c b/sys-tune/source/impl/aud_wrapper.c new file mode 100644 index 0000000..a1758df --- /dev/null +++ b/sys-tune/source/impl/aud_wrapper.c @@ -0,0 +1,78 @@ +#include "aud.h" +#include "audout.h" +#include "aud_wrapper.h" +#include + +// IAudioSystemManagerForApplet +Result audWrapperInitialize(void) { + if (hosversionBefore(11,0,0)) + return audoutaInitialize(); + else + return audaInitialize(); +} + +void audWrapperExit(void) { + if (hosversionBefore(11,0,0)) + audoutaInitialize(); + else + audaInitialize(); +} + +Result audWrapperRequestSuspend(u64 pid, u64 delay) { + Handle h; + if (hosversionBefore(4,0,0)) + return audoutaRequestSuspendOld(pid, delay, &h); + else if (hosversionBefore(11,0,0)) + return audoutaRequestSuspend(pid, delay); + else + return audaRequestSuspendAudio(pid, delay); +} + +Result audWrapperRequestResume(u64 pid, u64 delay) { + Handle h; + if (hosversionBefore(4,0,0)) + return audoutaRequestResumeOld(pid, delay, &h); + else if (hosversionBefore(11,0,0)) + return audoutaRequestResume(pid, delay); + else + return audaRequestResumeAudio(pid, delay); +} + +Result audWrapperGetProcessMasterVolume(u64 pid, float* volume_out) { + if (hosversionBefore(11,0,0)) + return audoutaGetProcessMasterVolume(pid, volume_out); + else + return audaGetAudioOutputProcessMasterVolume(pid, volume_out); +} + +Result audWrapperSetProcessMasterVolume(u64 pid, u64 delay, float volume) { + if (hosversionBefore(11,0,0)) + return audoutaSetProcessMasterVolume(pid, delay, volume); + else { + Result rc = audaSetAudioOutputProcessMasterVolume(pid, delay, volume); + if (R_FAILED(rc)) { + return rc; + } + return audaSetAudioInputProcessMasterVolume(pid, delay, volume); + } +} + +Result audWrapperGetProcessRecordVolume(u64 pid, float* volume_out) { + if (hosversionBefore(4,0,0)) + return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer); + + if (hosversionBefore(11,0,0)) + return audoutaGetProcessRecordVolume(pid, volume_out); + else + return audaGetAudioOutputProcessRecordVolume(pid, volume_out); +} + +Result audWrapperSetProcessRecordVolume(u64 pid, u64 delay, float volume) { + if (hosversionBefore(4,0,0)) + return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer); + + if (hosversionBefore(11,0,0)) + return audoutaSetProcessRecordVolume(pid, delay, volume); + else + return audaSetAudioOutputProcessRecordVolume(pid, delay, volume); +} diff --git a/sys-tune/source/impl/aud_wrapper.h b/sys-tune/source/impl/aud_wrapper.h new file mode 100644 index 0000000..d772814 --- /dev/null +++ b/sys-tune/source/impl/aud_wrapper.h @@ -0,0 +1,25 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define AUD_WRAPPER_MAX_DELAY (1000000000ULL) + +/// Initialize audout:a until [11.0.0], otherwise aud:a. +Result audWrapperInitialize(void); +/// Exit audout:a until [11.0.0], otherwise aud:a. +void audWrapperExit(void); + +Result audWrapperRequestSuspend(u64 pid, u64 delay); +Result audWrapperRequestResume(u64 pid, u64 delay); +Result audWrapperGetProcessMasterVolume(u64 pid, float* volume_out); +Result audWrapperSetProcessMasterVolume(u64 pid, u64 delay, float volume); +Result audWrapperGetProcessRecordVolume(u64 pid, float* volume_out); +Result audWrapperSetProcessRecordVolume(u64 pid, u64 delay, float volume); + +#ifdef __cplusplus +} +#endif diff --git a/sys-tune/source/impl/audout.c b/sys-tune/source/impl/audout.c new file mode 100644 index 0000000..595f271 --- /dev/null +++ b/sys-tune/source/impl/audout.c @@ -0,0 +1,138 @@ +#define NX_SERVICE_ASSUME_NON_DOMAIN +#include "audout.h" +#include + +static Service g_audoutaSrv; +static Service g_audoutdSrv; + +// AudioOutManagerForApplet +Result audoutaInitialize(void) { + if (hosversionAtLeast(11,0,0)) + return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer); + + return smGetService(&g_audoutaSrv, "audout:a"); +} + +void audoutaExit(void) { + serviceClose(&g_audoutaSrv); +} + +Service* audoutaGetServiceSession(void) { + return &g_audoutaSrv; +} + +Result audoutaRequestSuspendOld(u64 pid, u64 delay, Handle* handle_out) { + if (hosversionAtLeast(4,0,0)) + return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer); + + const struct { + u64 pid; + u64 timespan; + } in = { pid, delay }; + + return serviceDispatchInOut(&g_audoutaSrv, 0, in, *handle_out); +} + +Result audoutaRequestResumeOld(u64 pid, u64 delay, Handle* handle_out) { + if (hosversionAtLeast(4,0,0)) + return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer); + + const struct { + u64 pid; + u64 timespan; + } in = { pid, delay }; + + return serviceDispatchInOut(&g_audoutaSrv, 1, in, *handle_out); +} + +Result audoutaRequestSuspend(u64 pid, u64 delay) { + if (hosversionBefore(4,0,0)) + return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer); + + const struct { + u64 pid; + u64 timespan; + } in = { pid, delay }; + + return serviceDispatchIn(&g_audoutaSrv, 0, in); +} + +Result audoutaRequestResume(u64 pid, u64 delay) { + if (hosversionBefore(4,0,0)) + return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer); + + const struct { + u64 pid; + u64 timespan; + } in = { pid, delay }; + + return serviceDispatchIn(&g_audoutaSrv, 1, in); +} + +Result audoutaGetProcessMasterVolume(u64 pid, float* volume_out) { + return serviceDispatchInOut(&g_audoutaSrv, 2, pid, *volume_out); +} + +Result audoutaSetProcessMasterVolume(u64 pid, u64 delay, float volume) { + const struct { + float volume; + u64 pid; + u64 timespan; + } in = { volume, pid, 0 }; + + return serviceDispatchIn(&g_audoutaSrv, 3, in); +} + +Result audoutaGetProcessRecordVolume(u64 pid, float* volume_out) { + if (hosversionBefore(4,0,0)) + return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer); + + return serviceDispatchInOut(&g_audoutaSrv, 4, pid, *volume_out); +} + +Result audoutaSetProcessRecordVolume(u64 pid, u64 delay, float volume) { + if (hosversionBefore(4,0,0)) + return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer); + + const struct { + float volume; + u64 pid; + u64 timespan; + } in = { volume, pid, delay }; + + return serviceDispatchIn(&g_audoutaSrv, 5, in); +} + +// IAudioOutManagerForDebugger +Result audoutdInitialize(void) { + if (hosversionAtLeast(11,0,0)) + return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer); + + return smGetService(&g_audoutdSrv, "audout:d"); +} + +void audoutdExit(void) { + serviceClose(&g_audoutdSrv); +} + +Service* audoutdGetServiceSession(void) { + return &g_audoutdSrv; +} + +Result audoutdRequestSuspendForDebug(u64 pid, u64 delay) { + const struct { + u64 pid; + u64 timespan; + } in = { pid, delay }; + + return serviceDispatchIn(&g_audoutdSrv, 0, in); +} + +Result audoutdRequestResumeForDebug(u64 pid, u64 delay) { + const struct { + u64 pid; + u64 timespan; + } in = { pid, delay }; + + return serviceDispatchIn(&g_audoutdSrv, 1, in); +} diff --git a/sys-tune/source/impl/audout.h b/sys-tune/source/impl/audout.h new file mode 100644 index 0000000..89c8f34 --- /dev/null +++ b/sys-tune/source/impl/audout.h @@ -0,0 +1,50 @@ +/** + * @file audout.h + * @brief Removed in [11.0.0+], replaced with aud:a / aud:d (see aud.h) + * @note Only one session may be open at once. + * @author TotalJustice + * @copyright libnx Authors + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define AUDOUT_MAX_DELAY (1000000000ULL) + +/// Initialize audout:a. Removed in [11.0.0]. +Result audoutaInitialize(void); +/// Exit audout:a. +void audoutaExit(void); +/// Gets the Service for audout:a. +Service* audoutaGetServiceSession(void); + +/// Initialize audout:d. Removed in [11.0.0]. +Result audoutdInitialize(void); +/// Exit audout:d. +void audoutdExit(void); +/// Gets the Service for audout:d. +Service* audoutdGetServiceSession(void); + +// Untested +Result audoutaRequestSuspendOld(u64 pid, u64 delay, Handle* handle_out); // [1.0.0] - [4.0.0] +Result audoutaRequestResumeOld(u64 pid, u64 delay, Handle* handle_out); // [1.0.0] - [4.0.0] +// All tested (and works) +Result audoutaRequestSuspend(u64 pid, u64 delay); // [4.0.0]+ +Result audoutaRequestResume(u64 pid, u64 delay); // [4.0.0]+ +Result audoutaGetProcessMasterVolume(u64 pid, float* volume_out); +Result audoutaSetProcessMasterVolume(u64 pid, u64 delay, float volume); +Result audoutaGetProcessRecordVolume(u64 pid, float* volume_out); +Result audoutaSetProcessRecordVolume(u64 pid, u64 delay, float volume); + +// Untested +Result audoutdRequestSuspendForDebug(u64 pid, u64 delay); +Result audoutdRequestResumeForDebug(u64 pid, u64 delay); + +#ifdef __cplusplus +} +#endif diff --git a/sys-tune/source/impl/music_player.cpp b/sys-tune/source/impl/music_player.cpp index c620b30..d8c409e 100644 --- a/sys-tune/source/impl/music_player.cpp +++ b/sys-tune/source/impl/music_player.cpp @@ -1,30 +1,42 @@ #include "music_player.hpp" #include "../tune_result.hpp" -#include "sdmc.hpp" +#include "../tune_service.hpp" +#include "sdmc/sdmc.hpp" +#include "pm/pm.hpp" +#include "aud_wrapper.h" +#include "config/config.hpp" #include "source.hpp" -#include #include #include -#include -#include - -Source *g_source = nullptr; namespace tune::impl { namespace { + enum class AudrenCloseState { + None, // no change + Open, // just opened audren + Close, // just closed audren + }; + + constexpr float VOLUME_MAX = 1.f; - std::vector g_playlist; - std::vector g_shuffle_playlist; - std::string g_current = ""; + std::vector* g_playlist; + std::vector* g_shuffle_playlist; + PlaylistEntry* g_current; u32 g_queue_position; LockableMutex g_mutex; RepeatMode g_repeat = RepeatMode::All; ShuffleMode g_shuffle = ShuffleMode::Off; PlayerStatus g_status = PlayerStatus::FetchNext; + Source *g_source = nullptr; + + float g_tune_volume = 1.f; + float g_title_volume = 1.f; + float g_default_title_volume = 1.f; + bool g_use_title_volume = true; AudioDriver g_drv; constexpr const int MinSampleCount = 256; @@ -32,37 +44,110 @@ namespace tune::impl { constexpr const int BufferCount = 2; constexpr const int AudioSampleSize = MinSampleCount * MaxChannelCount * sizeof(s16); constexpr const int AudioPoolSize = AudioSampleSize * BufferCount; - alignas(0x1000) u8 AudioMemoryPool[AudioPoolSize]; + alignas(AUDREN_MEMPOOL_ALIGNMENT) u8 AudioMemoryPool[AudioPoolSize]; static_assert((sizeof(AudioMemoryPool) % 0x2000) == 0, "Audio Memory pool needs to be page aligned!"); - bool should_pause = false; - bool should_run = true; + bool g_should_pause = false; + bool g_should_run = true; + bool g_close_audren = false; + + Result audioInit() { + /* Default audio config. */ + const AudioRendererConfig audren_cfg = { + .output_rate = AudioRendererOutputRate_48kHz, + .num_voices = 2, + .num_effects = 0, + .num_sinks = 1, + .num_mix_objs = 1, + .num_mix_buffers = 2, + }; + + smInitialize(); + Result rc = audrenInitialize(&audren_cfg); + smExit(); + + if (R_SUCCEEDED(rc)) { + /* Create audio driver. */ + rc = audrvCreate(&g_drv, &audren_cfg, 2); + if (R_SUCCEEDED(rc)) { + /* Register memory pool. */ + int mpid = audrvMemPoolAdd(&g_drv, AudioMemoryPool, AudioPoolSize); + audrvMemPoolAttach(&g_drv, mpid); + + /* Attach default sink. */ + u8 sink_channels[] = {0, 1}; + audrvDeviceSinkAdd(&g_drv, AUDREN_DEFAULT_DEVICE_NAME, 2, sink_channels); + + rc = audrvUpdate(&g_drv); + if (R_SUCCEEDED(rc)) { + return audrenStartAudioRenderer(); + } else { + /* Cleanup on failure */ + audrvClose(&g_drv); + } + } else { + /* Cleanup on failure */ + audrenExit(); + } + } + + return rc; + } + + // Only call this from audrv thread, as closing audrv + // while accesing it will be very bad. + AudrenCloseState PollAudrenCloseState() { + static bool close_audren_previous = false; + + if (close_audren_previous != g_close_audren) { + close_audren_previous = g_close_audren; + if (g_close_audren) { + audrvClose(&g_drv); + audrenExit(); + return AudrenCloseState::Close; + } else { + audioInit(); + SetVolume(g_tune_volume); + return AudrenCloseState::Open; + } + } + + return AudrenCloseState::None; + } Result PlayTrack(const std::string &path) { /* Open file and allocate */ - auto source = std::unique_ptr(OpenFile(path.c_str())); + auto source = OpenFile(path.c_str()); R_UNLESS(source != nullptr, tune::FileOpenFailure); R_UNLESS(source->IsOpen(), tune::FileOpenFailure); - int channel_count = source->GetChannelCount(); - int sample_rate = source->GetSampleRate(); + const auto channel_count = source->GetChannelCount(); + const auto sample_rate = source->GetSampleRate(); - R_UNLESS(audrvVoiceInit(&g_drv, 0, channel_count, PcmFormat_Int16, sample_rate), tune::VoiceInitFailure); + const auto voice_init = [&]() -> Result { + R_UNLESS(audrvVoiceInit(&g_drv, 0, channel_count, PcmFormat_Int16, sample_rate), tune::VoiceInitFailure); - audrvVoiceSetDestinationMix(&g_drv, 0, AUDREN_FINAL_MIX_ID); + audrvVoiceSetDestinationMix(&g_drv, 0, AUDREN_FINAL_MIX_ID); - if (channel_count == 1) { - audrvVoiceSetMixFactor(&g_drv, 0, 1.0f, 0, 0); - audrvVoiceSetMixFactor(&g_drv, 0, 1.0f, 0, 1); - } else { - audrvVoiceSetMixFactor(&g_drv, 0, 1.0f, 0, 0); - audrvVoiceSetMixFactor(&g_drv, 0, 0.0f, 0, 1); - audrvVoiceSetMixFactor(&g_drv, 0, 0.0f, 1, 0); - audrvVoiceSetMixFactor(&g_drv, 0, 1.0f, 1, 1); - } + if (channel_count == 1) { + audrvVoiceSetMixFactor(&g_drv, 0, 1.0f, 0, 0); + audrvVoiceSetMixFactor(&g_drv, 0, 1.0f, 0, 1); + } else { + audrvVoiceSetMixFactor(&g_drv, 0, 1.0f, 0, 0); + audrvVoiceSetMixFactor(&g_drv, 0, 0.0f, 0, 1); + audrvVoiceSetMixFactor(&g_drv, 0, 0.0f, 1, 0); + audrvVoiceSetMixFactor(&g_drv, 0, 1.0f, 1, 1); + } - audrvVoiceStart(&g_drv, 0); + audrvVoiceStart(&g_drv, 0); + + return 0; + }; + + if (auto rc = voice_init(); R_FAILED(rc)) { + return rc; + } const s32 sample_count = AudioSampleSize / channel_count / sizeof(s16); AudioDriverWaveBuf buffers[BufferCount] = {}; @@ -76,8 +161,29 @@ namespace tune::impl { g_source = source.get(); - while (should_run && g_status == PlayerStatus::Playing) { - if (should_pause) { + while (g_should_run && g_status == PlayerStatus::Playing) { + switch (PollAudrenCloseState()) { + case AudrenCloseState::None: + break; + case AudrenCloseState::Open: + if (auto rc = voice_init(); R_FAILED(rc)) { + g_source = nullptr; + return rc; + } + break; + case AudrenCloseState::Close: + for (auto &buffer : buffers) { + buffer.state = AudioDriverWaveBufState_Free; + } + break; + } + + if (g_close_audren) { + svcSleepThread(100'000'000ul); + continue; + } + + if (g_should_pause) { svcSleepThread(17'000'000); continue; } @@ -120,64 +226,138 @@ namespace tune::impl { } - Result Initialize() { - /* Create audio driver. */ - Result rc = audrvCreate(&g_drv, &audren_cfg, 2); - if (R_SUCCEEDED(rc)) { - /* Register memory pool. */ - int mpid = audrvMemPoolAdd(&g_drv, AudioMemoryPool, AudioPoolSize); - audrvMemPoolAttach(&g_drv, mpid); + Result Initialize(std::vector* playlist, std::vector* shuffle, PlaylistEntry* current) { + g_playlist = playlist; + g_shuffle_playlist = shuffle; + g_current = current; + + // tldr, most fancy things made by N will fatal + const u64 blacklist[] = { + // https://github.com/HookedBehemoth/sys-tune/issues/10 + 0x010077B00E046000, // spyro reignited trilogy + 0x0100AD9012510000, // pac man 99 + 0x01006C100EC08000, // minecraft dugeons + 0x01000A10041EA000, // skyrim + 0x0100F9F00C696000, // crash team racing nitro fueled + 0x01001E9003502000, // labo 03 + 0x0100165003504000, // labo 04 + 0x0100F2300D4BA000, // darksiders genesis + 0x0100E1400BA96000, // darksiders warmastered edition + 0x010071800BA98000, // darksiders 2 + 0x0100F8F014190000, // darksiders 3 + 0x0100D870045B6000, // NES NSO + 0x01008D300C50C000, // SNES NSO + 0x0100C62011050000, // GB NSO + 0x010012F017576000, // GBA NSO + 0x0100C9A00ECE6000, // N64 NSO + + // https://github.com/tallbl0nde/TriPlayer/issues/31 + 0x0100E5600D446000, // Ni No Kuni: Wrath of the White Witch + 0x0100A3900C3E2000, // Paper Mario™: The Origami King + 0x0100626011656000, // The Outer Worlds + 0x010090F012916000, // Ghostrunner + 0x0100F15012D36000, // IMMERSE LAND + 0x01005950022EC000, // Blade Strangers + 0x0100423009358000, // Death Road to Canada + 0x010044500C182000, // Sid Meier's Civilization VI + + // anything made by PROTOTYPE + 0x0100A3A00CC7E000, // CLANNAD + 0x01007B501372C000, // CLANNAD Side Stories + 0x01003B300E4AA000, // THE GRISAIA TRILOGY + 0x0100F06013710000, // ISLAND + 0x0100BD100C752000, // planetarian + 0x01002330123BC000, // GRISAIA PHANTOM TRIGGER 05 + 0x0100240013AE8000, // GRISAIA PHANTOM TRIGGER 06 + 0x01002EF014DA2000, // GRISAIA PHANTOM TRIGGER 07 + 0x0100398010314000, // Tomoyo After -It's a Wonderful Life- CS Edition + 0x01004AB0133E8000, // GRISAIA PHANTOM TRIGGER 01 to 05 + 0x01005250123B8000, // GRISAIA PHANTOM TRIGGER 03 + 0x010054101370E000, // FATAL TWELVE + 0x010062A0178A8000, // LOOPERS + 0x0100806017562000, // OshiRabu: Waifus Over Husbandos + Love・or・die + 0x0100943010310000, // Little Busters! Converted Edition + 0x010096000CA38000, // TAISHO x ALICE ALL IN ONE + 0x0100A1200CA3C000, // Butterfly's Poison; Blood Chains + 0x0100C38019CE4000, // GRISAIA PHANTOM TRIGGER 08 + 0x0100C9C0178A6000, // Harmonia + 0x0100CAF013AE6000, // GRISAIA PHANTOM TRIGGER 5.5 + 0x0100D970123BA000, // GRISAIA PHANTOM TRIGGER 04 + }; + + // do this on startup because the user may not copy a config + // file or delete it at somepoint + for (auto tid : blacklist) { + config::set_title_blacklist(tid, true); + } - /* Attach default sink. */ - u8 sink_channels[] = {0, 1}; - audrvDeviceSinkAdd(&g_drv, AUDREN_DEFAULT_DEVICE_NAME, 2, sink_channels); + if (auto rc = audioInit(); R_FAILED(rc)) { + return rc; + } - rc = audrvUpdate(&g_drv); - if (R_SUCCEEDED(rc)) { - rc = audrenStartAudioRenderer(); - if (R_SUCCEEDED(rc)) - return 0; - } else { - /* Cleanup on failure */ - audrvClose(&g_drv); - } + /* Fetch values from config, sanitize the return value */ + if (auto c = config::get_repeat(); c <= 2 && c >= 0) { + SetRepeatMode(static_cast(c)); } - return rc; + SetShuffleMode(static_cast(config::get_shuffle())); + SetVolume(config::get_volume()); + SetDefaultTitleVolume(config::get_default_title_volume()); + + return 0; + } void Exit() { - should_run = false; + g_should_run = false; } void TuneThreadFunc(void *) { /* Run as long as we aren't stopped and no error has been encountered. */ - while (should_run) { - g_current = ""; + while (g_should_run) { + // update g_close_audren, returned state isn't needed + PollAudrenCloseState(); + + if (g_close_audren) { + svcSleepThread(100'000'000ul); + continue; + } + + g_current->path = ""; { std::scoped_lock lk(g_mutex); - auto &queue = (g_shuffle == ShuffleMode::On) ? g_shuffle_playlist : g_playlist; - size_t queue_size = queue.size(); + const auto &queue = *g_playlist; + const auto queue_size = queue.size(); if (queue_size == 0) { - g_current = ""; + g_current->path = ""; } else if (g_queue_position >= queue_size) { g_queue_position = queue_size - 1; continue; } else { - g_current = queue[g_queue_position]; + if (g_shuffle == ShuffleMode::On) { + const auto shuffle_id = (*g_shuffle_playlist)[g_queue_position]; + for (u32 i = 0; i < g_playlist->size(); i++) { + if ((*g_playlist)[i].id == shuffle_id) { + *g_current = (*g_playlist)[i]; + break; + } + } + } else { + *g_current = queue[g_queue_position]; + } } } /* Sleep if queue is empty. */ - if (g_current.empty()) { + if (g_current->path.empty()) { svcSleepThread(100'000'000ul); continue; } g_status = PlayerStatus::Playing; /* Only play if playing and we have a track queued. */ - Result rc = PlayTrack(g_current); + Result rc = PlayTrack(g_current->path); /* Log error. */ if (R_FAILED(rc)) { @@ -191,13 +371,19 @@ namespace tune::impl { } } - audrvClose(&g_drv); + if (!g_close_audren) { + audrvClose(&g_drv); + // this needs to be closed asap if a blacklisted title is launched. + // this is why we close this here rather than in __appExit + audrenExit(); + } } void PscmThreadFunc(void *ptr) { PscPmModule *module = static_cast(ptr); + bool previous_state{}; - while (should_run) { + while (g_should_run) { Result rc = eventWait(&module->event, 10'000'000); if (R_VALUE(rc) == KERNELRESULT(TimedOut)) continue; @@ -208,8 +394,16 @@ namespace tune::impl { u32 flags; R_ABORT_UNLESS(pscPmModuleGetRequest(module, &state, &flags)); switch (state) { + // NOTE: PscPmState_Awake event seems to get missed (rare) or + // PscPmState_ReadySleep is sent multiple times. + // todo: fade in and delay playback on wakeup slightly + case PscPmState_ReadyAwaken: + g_should_pause = previous_state; + break; + // pause on sleep case PscPmState_ReadySleep: - should_pause = true; + previous_state = g_should_pause; + g_should_pause = true; break; default: break; @@ -226,16 +420,16 @@ namespace tune::impl { /* [0] Low == plugged in; [1] High == not plugged in. */ GpioValue old_value = GpioValue_High; - while (should_run) { + while (g_should_run) { /* Fetch current gpio value. */ GpioValue value; if (R_SUCCEEDED(gpioPadGetValue(session, &value))) { if (old_value == GpioValue_Low && value == GpioValue_High) { - pre_unplug_pause = should_pause; - should_pause = true; + pre_unplug_pause = g_should_pause; + g_should_pause = true; } else if (old_value == GpioValue_High && value == GpioValue_Low) { if (!pre_unplug_pause) - should_pause = false; + g_should_pause = false; } old_value = value; } @@ -243,16 +437,64 @@ namespace tune::impl { } } + void PmdmntThreadFunc(void *ptr) { + u64 previous_tid{}; + u64 current_tid{}; + bool previous_state{}; + + while (g_should_run) { + u64 pid{}, new_tid{}; + if (pm::PollCurrentPidTid(&pid, &new_tid)) { + // check if title is blacklisted + g_close_audren = config::get_title_blacklist(new_tid); + + g_title_volume = 1.f; + + if (config::has_title_volume(new_tid)) { + g_use_title_volume = true; + SetTitleVolume(std::clamp(config::get_title_volume(new_tid), 0.f, VOLUME_MAX)); + } + + if (new_tid == previous_tid) { + g_should_pause = previous_state; + } else { + previous_state = g_should_pause; + // TODO: fade song in rather than abruptly playing to avoid jump scares + if (config::has_title_enabled(new_tid)) { + g_should_pause = !config::get_title_enabled(new_tid); + } else { + g_should_pause = !config::get_title_enabled_default(); + } + } + + previous_tid = current_tid; + current_tid = new_tid; + } + + // sadly, we can't simply apply auda when the title changes + // as it seems to apply to quickly, before the title opens audio + // services, so the changes don't apply. + // best option is to repeatdly set the out :/ + if (pid) { + const auto v = g_use_title_volume ? g_title_volume : g_default_title_volume; + audWrapperSetProcessMasterVolume(pid, 0, v); + // audWrapperSetProcessRecordVolume(pid, 0, v); + } + + svcSleepThread(10'000'000); + } + } + bool GetStatus() { - return !should_pause; + return !g_should_pause; } void Play() { - should_pause = false; + g_should_pause = false; } void Pause() { - should_pause = true; + g_should_pause = true; } void Next() { @@ -260,7 +502,7 @@ namespace tune::impl { { std::scoped_lock lk(g_mutex); - if (g_queue_position < g_playlist.size() - 1) { + if (g_queue_position < g_playlist->size() - 1) { g_queue_position++; } else { g_queue_position = 0; @@ -269,7 +511,7 @@ namespace tune::impl { } } g_status = PlayerStatus::FetchNext; - should_pause = pause; + g_should_pause = pause; } void Prev() { @@ -279,11 +521,11 @@ namespace tune::impl { if (g_queue_position > 0) { g_queue_position--; } else { - g_queue_position = g_playlist.size() - 1; + g_queue_position = g_playlist->size() - 1; } } g_status = PlayerStatus::FetchNext; - should_pause = false; + g_should_pause = false; } float GetVolume() { @@ -291,7 +533,29 @@ namespace tune::impl { } void SetVolume(float volume) { - g_drv.in_mixes[0].volume = volume; + volume = std::clamp(volume, 0.f, VOLUME_MAX); + g_tune_volume = g_drv.in_mixes[0].volume = volume; + config::set_volume(volume); + } + + float GetTitleVolume() { + return g_title_volume; + } + + void SetTitleVolume(float volume) { + volume = std::clamp(volume, 0.f, VOLUME_MAX); + g_title_volume = volume; + g_use_title_volume = true; + } + + float GetDefaultTitleVolume() { + return g_default_title_volume; + } + + void SetDefaultTitleVolume(float volume) { + volume = std::clamp(volume, 0.f, VOLUME_MAX); + g_default_title_volume = volume; + config::set_default_title_volume(volume); } RepeatMode GetRepeatMode() { @@ -309,13 +573,13 @@ namespace tune::impl { void SetShuffleMode(ShuffleMode mode) { std::scoped_lock lk(g_mutex); - if (g_playlist.size() > 0 && g_shuffle != mode) { - auto &dst = (mode == ShuffleMode::On) ? g_shuffle_playlist : g_playlist; + // if (g_playlist->size() > 0 && g_shuffle != mode) { + // auto &dst = (mode == ShuffleMode::On) ? *g_shuffle_playlist : *g_playlist; - auto it = std::find(dst.cbegin(), dst.cend(), g_current); - if (it != dst.cend()) - g_queue_position = it - dst.cbegin(); - } + // auto it = std::find(dst.cbegin(), dst.cend(), *g_current); + // if (it != dst.cend()) + // g_queue_position = std::distance(dst.cbegin(), it); + // } g_shuffle = mode; } @@ -323,16 +587,16 @@ namespace tune::impl { u32 GetPlaylistSize() { std::scoped_lock lk(g_mutex); - return g_playlist.size(); + return g_playlist->size(); } Result GetPlaylistItem(u32 index, char *buffer, size_t buffer_size) { std::scoped_lock lk(g_mutex); - if (index >= g_playlist.size()) + if (index >= g_playlist->size()) return tune::OutOfRange; - strncpy(buffer, g_playlist[index].c_str(), buffer_size); + std::strncpy(buffer, (*g_playlist)[index].path.c_str(), buffer_size); return 0; } @@ -343,9 +607,9 @@ namespace tune::impl { { std::scoped_lock lk(g_mutex); - R_UNLESS(!g_current.empty(), tune::NotPlaying); - R_UNLESS(buffer_size >= g_current.size(), tune::InvalidArgument); - std::strcpy(buffer, g_current.c_str()); + R_UNLESS(!g_current->path.empty(), tune::NotPlaying); + R_UNLESS(buffer_size >= g_current->path.size(), tune::InvalidArgument); + std::strcpy(buffer, g_current->path.c_str()); } auto [current, total] = g_source->Tell(); @@ -362,8 +626,8 @@ namespace tune::impl { { std::scoped_lock lk(g_mutex); - g_playlist.clear(); - g_shuffle_playlist.clear(); + g_playlist->clear(); + g_shuffle_playlist->clear(); } g_status = PlayerStatus::FetchNext; } @@ -371,7 +635,7 @@ namespace tune::impl { void MoveQueueItem(u32 src, u32 dst) { std::scoped_lock lk(g_mutex); - size_t queue_size = g_playlist.size(); + const auto queue_size = g_playlist->size(); if (src >= queue_size) { src = queue_size - 1; @@ -380,11 +644,11 @@ namespace tune::impl { dst = queue_size - 1; } - auto source = g_playlist.cbegin() + src; - auto dest = g_playlist.cbegin() + dst; + auto source = g_playlist->cbegin() + src; + auto dest = g_playlist->cbegin() + dst; - g_playlist.insert(dest, *source); - g_playlist.erase(source); + g_playlist->insert(dest, *source); + g_playlist->erase(source); if (src < dst) { if (g_queue_position == src) { @@ -406,22 +670,22 @@ namespace tune::impl { std::scoped_lock lk(g_mutex); /* Check if we are out of bounds. */ - size_t queue_size = g_playlist.size(); + size_t queue_size = g_playlist->size(); if (index >= queue_size) { index = queue_size - 1; } /* Get absolute position in current playlist. Independent of shufflemode. */ - u32 pos = UINT32_MAX; + u32 pos = index; + if (g_shuffle == ShuffleMode::On) { - auto it = std::find(g_shuffle_playlist.cbegin(), g_shuffle_playlist.cend(), g_playlist[index]); - if (it != g_shuffle_playlist.cend()) { - pos = std::distance(it, g_shuffle_playlist.cbegin()); - } else { - return; + const auto track = g_playlist->cbegin() + index; + for (u32 i = 0; i < g_shuffle_playlist->size(); i++) { + if ((*g_shuffle_playlist)[i] == track->id) { + pos = i; + break; + } } - } else { - pos = index; } /* Return if that track is already selected. */ @@ -431,7 +695,7 @@ namespace tune::impl { g_queue_position = pos; } g_status = PlayerStatus::FetchNext; - should_pause = false; + g_should_pause = false; } void Seek(u32 position) { @@ -440,21 +704,37 @@ namespace tune::impl { } Result Enqueue(const char *buffer, size_t buffer_length, EnqueueType type) { + // NOTE: do not decrement this + static PlaylistID playlist_id{}; + /* Ensure file exists. */ if (!sdmc::FileExists(buffer)) return tune::InvalidPath; std::scoped_lock lk(g_mutex); + const PlaylistEntry new_entry{ + .path = {buffer, buffer_length}, + .id = playlist_id + }; + + // add new entry to playlist if (type == EnqueueType::Front) { - g_playlist.emplace(g_playlist.cbegin(), buffer, buffer_length); - g_queue_position++; + g_playlist->emplace(g_playlist->cbegin(), new_entry); + if (g_shuffle == ShuffleMode::Off) { + g_queue_position++; + } } else { - g_playlist.emplace_back(buffer, buffer_length); + g_playlist->emplace_back(new_entry); } - size_t shuffle_playlist_size = g_shuffle_playlist.size(); - size_t shuffle_index = (shuffle_playlist_size > 1) ? (randomGet64() % shuffle_playlist_size) : 0; - g_shuffle_playlist.emplace(g_shuffle_playlist.cbegin() + shuffle_index, buffer, buffer_length); + + // add new entry id to shuffle_playlist_list + const auto shuffle_playlist_size = g_shuffle_playlist->size(); + const auto shuffle_index = (shuffle_playlist_size > 1) ? (randomGet64() % shuffle_playlist_size) : 0; + g_shuffle_playlist->emplace(g_shuffle_playlist->cbegin() + shuffle_index, playlist_id); + + // increase playlist counter + playlist_id++; return 0; } @@ -463,23 +743,27 @@ namespace tune::impl { std::scoped_lock lk(g_mutex); /* Ensure we don't operate out of bounds. */ - R_UNLESS(!g_playlist.empty(), tune::QueueEmpty); - R_UNLESS(index < g_playlist.size(), tune::OutOfRange); + R_UNLESS(!g_playlist->empty(), tune::QueueEmpty); + R_UNLESS(index < g_playlist->size(), tune::OutOfRange); /* Get iterator for index position. */ - auto track = g_playlist.cbegin() + index; - - auto shuffle_it = std::find(g_shuffle_playlist.cbegin(), g_shuffle_playlist.cend(), *track); + const auto track = g_playlist->cbegin() + index; + + for (u32 i = 0; i < g_shuffle_playlist->size(); i++) { + if ((*g_shuffle_playlist)[i] == track->id) { + const auto shuffle_it = g_shuffle_playlist->cbegin() + i; + // we are playing from shuffle list so use that index instead + if (g_shuffle == ShuffleMode::On) { + index = i; + } + // finally remove + g_shuffle_playlist->erase(shuffle_it); + break; + } + } /* Remove entry. */ - g_playlist.erase(track); - - if (shuffle_it != g_shuffle_playlist.cend()) { - g_shuffle_playlist.erase(shuffle_it); - - if (g_shuffle == ShuffleMode::On) - index = std::distance(shuffle_it, g_shuffle_playlist.cbegin()); - } + g_playlist->erase(track); /* Fetch a new track if we deleted the current song. */ bool fetch_new = g_queue_position == index; diff --git a/sys-tune/source/impl/music_player.hpp b/sys-tune/source/impl/music_player.hpp index 0a4695b..df1db9b 100644 --- a/sys-tune/source/impl/music_player.hpp +++ b/sys-tune/source/impl/music_player.hpp @@ -1,25 +1,24 @@ #pragma once #include "../tune_types.hpp" - -/* Default audio config. */ -constexpr const AudioRendererConfig audren_cfg = { - .output_rate = AudioRendererOutputRate_48kHz, - .num_voices = 4, - .num_effects = 0, - .num_sinks = 1, - .num_mix_objs = 1, - .num_mix_buffers = 2, -}; +#include +#include namespace tune::impl { + using PlaylistID = u32; + + struct PlaylistEntry { + std::string path; + PlaylistID id; + }; - Result Initialize(); + Result Initialize(std::vector* playlist, std::vector* shuffle, PlaylistEntry* current); void Exit(); void TuneThreadFunc(void *); void PscmThreadFunc(void *ptr); void GpioThreadFunc(void *ptr); + void PmdmntThreadFunc(void *ptr); bool GetStatus(); void Play(); @@ -29,6 +28,15 @@ namespace tune::impl { float GetVolume(); void SetVolume(float volume); + float GetTitleVolume(); + void SetTitleVolume(float volume); + float GetDefaultTitleVolume(); + void SetDefaultTitleVolume(float volume); + + void TitlePlay(); + void TitlePause(); + void DefaultTitlePlay(); + void DefaultTitlePause(); RepeatMode GetRepeatMode(); void SetRepeatMode(RepeatMode mode); diff --git a/sys-tune/source/impl/source.cpp b/sys-tune/source/impl/source.cpp index ca2ffae..e8764de 100644 --- a/sys-tune/source/impl/source.cpp +++ b/sys-tune/source/impl/source.cpp @@ -1,6 +1,6 @@ #include "source.hpp" -#include "sdmc.hpp" +#include "sdmc/sdmc.hpp" #include #include @@ -12,7 +12,7 @@ #define DR_MP3_IMPLEMENTATION #define DR_MP3_NO_STDIO -#define DRMP3_DATA_CHUNK_SIZE 0x4000 * 6 +#define DRMP3_DATA_CHUNK_SIZE (1024 * MP3_CHUNK_SIZE_KB) #include "dr_mp3.h" #define DR_WAV_IMPLEMENTATION @@ -125,7 +125,7 @@ bool Source::Done() { return current == total; } -class FlacFile : public Source { +class FlacFile final : public Source { private: drflac *m_flac; @@ -138,38 +138,38 @@ class FlacFile : public Source { drflac_close(this->m_flac); } - bool IsOpen() { + bool IsOpen() override { return this->m_flac != nullptr; } - size_t Decode(size_t sample_count, s16 *data) { + size_t Decode(size_t sample_count, s16 *data) override { std::scoped_lock lk(this->m_mutex); return drflac_read_pcm_frames_s16(this->m_flac, sample_count, data); } - std::pair Tell() { + std::pair Tell() override { std::scoped_lock lk(this->m_mutex); return {this->m_flac->currentPCMFrame, this->m_flac->totalPCMFrameCount}; } - bool Seek(u64 target) { + bool Seek(u64 target) override { std::scoped_lock lk(this->m_mutex); return drflac_seek_to_pcm_frame(this->m_flac, target); } - int GetSampleRate() { + int GetSampleRate() override { return this->m_flac->sampleRate; } - int GetChannelCount() { + int GetChannelCount() override { return this->m_flac->channels; } }; -class Mp3File : public Source { +class Mp3File final : public Source { private: drmp3 m_mp3; bool initialized; @@ -187,38 +187,38 @@ class Mp3File : public Source { drmp3_uninit(&this->m_mp3); } - bool IsOpen() { + bool IsOpen() override { return initialized; } - size_t Decode(size_t sample_count, s16 *data) { + size_t Decode(size_t sample_count, s16 *data) override { std::scoped_lock lk(this->m_mutex); return drmp3_read_pcm_frames_s16(&this->m_mp3, sample_count, data); } - std::pair Tell() { + std::pair Tell() override { std::scoped_lock lk(this->m_mutex); return {this->m_mp3.currentPCMFrame, this->m_total_frame_count}; } - bool Seek(u64 target) { + bool Seek(u64 target) override { std::scoped_lock lk(this->m_mutex); return drmp3_seek_to_pcm_frame(&this->m_mp3, target); } - int GetSampleRate() { + int GetSampleRate() override { return this->m_mp3.sampleRate; } - int GetChannelCount() { + int GetChannelCount() override { return this->m_mp3.channels; } }; -class WavFile : public Source { +class WavFile final : public Source { private: drwav m_wav; bool initialized; @@ -236,40 +236,40 @@ class WavFile : public Source { drwav_uninit(&this->m_wav); } - bool IsOpen() { + bool IsOpen() override { return initialized; } - size_t Decode(size_t sample_count, s16 *data) { + size_t Decode(size_t sample_count, s16 *data) override { std::scoped_lock lk(this->m_mutex); return drwav_read_pcm_frames_s16(&this->m_wav, sample_count, data); } - std::pair Tell() { + std::pair Tell() override { std::scoped_lock lk(this->m_mutex); u64 byte_position = this->m_wav.dataChunkDataSize - this->m_wav.bytesRemaining; return {byte_position / this->m_bytes_per_pcm, this->m_wav.totalPCMFrameCount}; } - bool Seek(u64 target) { + bool Seek(u64 target) override { std::scoped_lock lk(this->m_mutex); return drwav_seek_to_pcm_frame(&this->m_wav, target); } - int GetSampleRate() { + int GetSampleRate() override { return this->m_wav.sampleRate; } - int GetChannelCount() { + int GetChannelCount() override { return this->m_wav.channels; } }; -Source *OpenFile(const char *path) { - size_t length = std::strlen(path); +std::unique_ptr OpenFile(const char *path) { + const auto length = std::strlen(path); if (length < 5) return nullptr; @@ -278,27 +278,24 @@ Source *OpenFile(const char *path) { if (R_FAILED(sdmc::OpenFile(&file, path))) return nullptr; - Source *source = nullptr; - if (false) {} #ifdef WANT_MP3 else if (strcasecmp(path + length - 4, ".mp3") == 0) { - source = new (std::nothrow) Mp3File(std::move(file)); + return std::make_unique(std::move(file)); } #endif #ifdef WANT_FLAC else if (strcasecmp(path + length - 5, ".flac") == 0) { - source = new (std::nothrow) FlacFile(std::move(file)); + return std::make_unique(std::move(file)); } #endif #ifdef WANT_WAV else if (strcasecmp(path + length - 4, ".wav") == 0 || strcasecmp(path + length - 5, ".wave") == 0) { - source = new (std::nothrow) WavFile(std::move(file)); + return std::make_unique(std::move(file)); } #endif - - if (source == nullptr) + else { fsFileClose(&file); - - return source; + return nullptr; + } } diff --git a/sys-tune/source/impl/source.hpp b/sys-tune/source/impl/source.hpp index e373528..f3e8f49 100644 --- a/sys-tune/source/impl/source.hpp +++ b/sys-tune/source/impl/source.hpp @@ -1,6 +1,10 @@ #pragma once #include +#include + +// number of kb to allocate for mp3 chunk +#define MP3_CHUNK_SIZE_KB 96 class Source { private: @@ -29,4 +33,4 @@ class Source { virtual int GetChannelCount() = 0; }; -Source *OpenFile(const char *path); +std::unique_ptr OpenFile(const char *path); diff --git a/sys-tune/source/main.cpp b/sys-tune/source/main.cpp index 29c43a2..8e40869 100644 --- a/sys-tune/source/main.cpp +++ b/sys-tune/source/main.cpp @@ -1,5 +1,7 @@ #include "impl/music_player.hpp" -#include "impl/sdmc.hpp" +#include "sdmc/sdmc.hpp" +#include "pm/pm.hpp" +#include "impl/aud_wrapper.h" #include "impl/source.hpp" #include "tune_service.hpp" #include "tune_result.hpp" @@ -8,25 +10,24 @@ extern "C" { u32 __nx_applet_type = AppletType_None; u32 __nx_fs_num_sessions = 1; -#define INNER_HEAP_SIZE 0x60000 -size_t nx_inner_heap_size = INNER_HEAP_SIZE; -char nx_inner_heap[INNER_HEAP_SIZE]; - -void __libnx_initheap(void); -void __appInit(void); -void __appExit(void); -} +// do not decrease this, will either cause fatal or will fail to start +// - 1024 * 216: needed for sys-tune to boot +// - 1024 * 236: base +// - 1024 * 268: needed for mp3 playback (at 32kb) +// - 1024 * 300: needed for mp3 playback (at 64kb) +// - 1024 * 332: needed for mp3 playback (at 96kb) +// - 1024 * 364: needed for closing / reopening audrv and audren (mp3 at 0kb) +// - 1024 * 460: needed for closing / reopening audrv and audren (mp3 at 96kb) +#define INNER_HEAP_SIZE 1024 * (364 + MP3_CHUNK_SIZE_KB) void __libnx_initheap(void) { - void *addr = nx_inner_heap; - size_t size = nx_inner_heap_size; - - /* Newlib */ + static char inner_heap[INNER_HEAP_SIZE]; extern char *fake_heap_start; extern char *fake_heap_end; - fake_heap_start = (char *)addr; - fake_heap_end = (char *)addr + size; + // Configure the newlib heap. + fake_heap_start = inner_heap; + fake_heap_end = inner_heap + sizeof(inner_heap); } void __appInit() { @@ -41,31 +42,38 @@ void __appInit() { R_ABORT_UNLESS(gpioInitialize()); R_ABORT_UNLESS(pscmInitialize()); - R_ABORT_UNLESS(audrenInitialize(&audren_cfg)); R_ABORT_UNLESS(fsInitialize()); + R_ABORT_UNLESS(audWrapperInitialize()); + R_ABORT_UNLESS(pm::Initialize()); R_ABORT_UNLESS(sdmc::Open()); smExit(); } void __appExit(void) { sdmc::Close(); - + pm::Exit(); + audWrapperExit(); fsExit(); - audrenExit(); pscmExit(); gpioExit(); } +} // extern "C" + namespace { alignas(0x1000) u8 gpioThreadBuffer[0x1000]; alignas(0x1000) u8 pscmThreadBuffer[0x1000]; + alignas(0x1000) u8 pmdmntThreadBuffer[0x1000]; alignas(0x1000) u8 tuneThreadBuffer[0x6000]; } int main(int argc, char *argv[]) { - R_ABORT_UNLESS(tune::impl::Initialize()); + std::vector playlist; + std::vector shuffle; + tune::impl::PlaylistEntry current; + R_ABORT_UNLESS(tune::impl::Initialize(&playlist, &shuffle, ¤t)); /* Register audio as our dependency so we can pause before it prepares for sleep. */ constexpr const u32 dependencies[] = { PscPmModuleId_Audio }; @@ -80,13 +88,16 @@ int main(int argc, char *argv[]) { ::Thread gpioThread; ::Thread pscmThread; + ::Thread pmdmtThread; ::Thread tuneThread; - R_ABORT_UNLESS(threadCreate(&gpioThread, tune::impl::GpioThreadFunc, &headphone_detect_session, gpioThreadBuffer, 0x1000, 0x20, -2)); - R_ABORT_UNLESS(threadCreate(&pscmThread, tune::impl::PscmThreadFunc, &pm_module, pscmThreadBuffer, 0x1000, 0x20, -2)); - R_ABORT_UNLESS(threadCreate(&tuneThread, tune::impl::TuneThreadFunc, nullptr, tuneThreadBuffer, 0x6000, 0x20, -2)); + R_ABORT_UNLESS(threadCreate(&gpioThread, tune::impl::GpioThreadFunc, &headphone_detect_session, gpioThreadBuffer, sizeof(gpioThreadBuffer), 0x20, -2)); + R_ABORT_UNLESS(threadCreate(&pscmThread, tune::impl::PscmThreadFunc, &pm_module, pscmThreadBuffer, sizeof(pscmThreadBuffer), 0x20, -2)); + R_ABORT_UNLESS(threadCreate(&pmdmtThread, tune::impl::PmdmntThreadFunc, nullptr, pmdmntThreadBuffer, sizeof(pmdmntThreadBuffer), 0x20, -2)); + R_ABORT_UNLESS(threadCreate(&tuneThread, tune::impl::TuneThreadFunc, nullptr, tuneThreadBuffer, sizeof(tuneThreadBuffer), 0x20, -2)); R_ABORT_UNLESS(threadStart(&gpioThread)); R_ABORT_UNLESS(threadStart(&pscmThread)); + R_ABORT_UNLESS(threadStart(&pmdmtThread)); R_ABORT_UNLESS(threadStart(&tuneThread)); /* Create services */ @@ -100,10 +111,12 @@ int main(int argc, char *argv[]) { R_ABORT_UNLESS(threadWaitForExit(&gpioThread)); R_ABORT_UNLESS(threadWaitForExit(&pscmThread)); + R_ABORT_UNLESS(threadWaitForExit(&pmdmtThread)); R_ABORT_UNLESS(threadWaitForExit(&tuneThread)); R_ABORT_UNLESS(threadClose(&gpioThread)); R_ABORT_UNLESS(threadClose(&pscmThread)); + R_ABORT_UNLESS(threadClose(&pmdmtThread)); R_ABORT_UNLESS(threadClose(&tuneThread)); /* Close gpio session. */ diff --git a/sys-tune/source/tune_service.cpp b/sys-tune/source/tune_service.cpp index 8522bbe..4a4530b 100644 --- a/sys-tune/source/tune_service.cpp +++ b/sys-tune/source/tune_service.cpp @@ -57,6 +57,18 @@ namespace tune { case TuneIpcCmd_SetVolume: SET_SINGLE(float, impl::SetVolume); + case TuneIpcCmd_GetTitleVolume: + GET_SINGLE(float, impl::GetTitleVolume); + + case TuneIpcCmd_SetTitleVolume: + SET_SINGLE(float, impl::SetTitleVolume); + + case TuneIpcCmd_GetDefaultTitleVolume: + GET_SINGLE(float, impl::GetDefaultTitleVolume); + + case TuneIpcCmd_SetDefaultTitleVolume: + SET_SINGLE(float, impl::SetDefaultTitleVolume); + case TuneIpcCmd_GetRepeatMode: GET_SINGLE(RepeatMode, impl::GetRepeatMode); diff --git a/sys-tune/toolbox.json b/sys-tune/toolbox.json new file mode 100644 index 0000000..511caf1 --- /dev/null +++ b/sys-tune/toolbox.json @@ -0,0 +1,5 @@ +{ + "name" : "sys-tune", + "tid" : "4200000000000000", + "requires_reboot": false +}