mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Play the sound on a chosen output device: the audio_device setting and --audio-device pick it by index or part of its name, --list-audio-devices prints the devices, and a chosen device that goes away hands the output to the default one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,10 @@ The game's *Swap Jump Controls* option trades B and X (jump on B, put away on X)
|
||||
|
||||
The Minus+Plus combo never reaches the game. In the menu the arrow keys, the D-pad or the left stick move, Enter or A selects, Q and E or L and R switch tabs, Esc or B goes back (Switch button names, as in the table above).
|
||||
|
||||
## Audio output
|
||||
|
||||
The sound plays on the system default device, and follows it when the Windows default changes. `NX2NAVI_AUDIO_DEVICE` (`audio_device` in `nx2navi.ini`, section `[audio]`) names another one: a number counting from 1 in the order `nx2navi.exe --list-audio-devices` prints, or part of the device's name, ignoring case. `--list-audio-devices` prints each playback device's index, name, channels and frequency without game files, marks the system default, and shows which device the setting picks; `nx2navi.exe --audio-device <value>` sets `NX2NAVI_AUDIO_DEVICE` for one run and wins over the file. When nothing matches or the device will not open, the log lists the playback devices and the game plays on the system default; a chosen device that disappears while the game runs (a virtual sink removed, a headset unplugged) also moves the sound to the system default. `audio_channels = auto` asks the chosen device how many channels it has.
|
||||
|
||||
## Remapping
|
||||
|
||||
The *Controls* tab of the settings menu lists, under the button layout, every Switch button and the eight stick directions the keys drive (movement and camera), each with two key cells and a controller cell. Each row's description says what the button does in the game. Select a cell, press A or Enter, then press the new key, mouse button or controller button; Esc or five seconds without a press cancel, and a controller button cancels a key cell. Delete, Backspace or X clears the selected cell. A key another action has moves to the new one (the description names the action it left); a controller button swaps with the one it replaces. A remapped controller button is physical: from then on the face buttons (A B X Y) stay where they are in both layouts, until the buttons are put back as the layout in use has them or *Reset all buttons* is chosen. F1, F8, F10, F11 and Esc cannot be bound (the menu, the flight recorder and the window keep them). Punctuation keys are named by their place on a US keyboard; the menu shows the character the keyboard layout in use puts there.
|
||||
|
||||
@@ -351,6 +351,12 @@
|
||||
# Speakers: stereo, 5.1 or auto (5.1 when the Windows speaker setup has six or more channels, stereo otherwise). The
|
||||
# game is told the same channel count and mixes for it. Takes effect at the next start.
|
||||
# audio_channels = stereo
|
||||
# Which output device the sound plays on. default (the default): the system default device, which the game follows when
|
||||
# the Windows default changes. Or a number, counting from 1 in the order `nx2navi.exe --list-audio-devices` prints; or part
|
||||
# of a device's name, ignoring case (`headphones`). When nothing matches, or the device will not open, the log lists the
|
||||
# devices and the game plays on the system default. With audio_channels = auto, the channel count is the chosen device's.
|
||||
# The command line `--audio-device <value>` overrides this for one run. Takes effect at the next start.
|
||||
# audio_device = default
|
||||
|
||||
[input]
|
||||
# Gamepad motion controls (0 = off) and vibration (0 = off).
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// with NX2NAVI_AUDIO_CHANNELS, output_channels) into a single-producer single-consumer ring. The SDL audio thread drains
|
||||
// that ring without taking any lock: an empty ring
|
||||
// plays silence and never blocks the render thread or the guest. NX2NAVI_AUDIO=none (or NX2NAVI_AUDIO_OFF) mixes without a
|
||||
// device; NX2NAVI_AUDIO_PREFILL_MS (0..500, default 20, 60 on Linux) is the queued audio the output waits for after start or underrun.
|
||||
// device; NX2NAVI_AUDIO_DEVICE picks the playback device (resolve_audio_device); NX2NAVI_AUDIO_PREFILL_MS (0..500, default 20, 60 on Linux) is the queued audio the output waits for after start or underrun.
|
||||
// The device's clock paces the render thread, as the audio hardware paces the Switch's DSP (audio_mixer_pace_ns): a
|
||||
// frame is rendered when the ring holds less than the output target, max(prefill, two device periods), so the queue and
|
||||
// the delay from the game to the speakers stay there and the host and device clocks cannot drift apart.
|
||||
@@ -54,6 +54,9 @@ typedef struct Mixer {
|
||||
int *offset; // first mix buffer of each mix id
|
||||
SplitInfo *split; int nsplit; SplitDest *dest; int ndest;
|
||||
SDL_AudioStream *stream; int device_ok, rate;
|
||||
SDL_AudioDeviceID device; // the explicitly chosen device, 0 while the system default is open
|
||||
char device_name[128]; // its name, for the log when it goes away
|
||||
uint64_t watch_ns; // render thread: when device_check last looked
|
||||
int channels; // output channels, 2 or 6 (output_channels)
|
||||
float ring[RING_FRAMES * OUT_MAX];
|
||||
_Atomic uint64_t wr, rd, underruns, overruns, missing_frames, callback_frames, callback_nonzero, skipped_frames;
|
||||
@@ -114,8 +117,116 @@ static int prefill_blocks_for(const char *e) {
|
||||
return (int)ceil(ms / 5.0);
|
||||
}
|
||||
|
||||
// ---- playback device choice (NX2NAVI_AUDIO_DEVICE)
|
||||
// SDL's playback device ids sorted by name (ties by id), the order --list-audio-devices prints them and the numbering of
|
||||
// the index form. SDL's own order is not stable: it differed between two enumerations in one process, so an index into it
|
||||
// would pick another device from one start to the next. Free the result with SDL_free. SDL audio must be initialised.
|
||||
// *count is 0 when SDL reports none.
|
||||
static int device_order(const void *x, const void *y) {
|
||||
SDL_AudioDeviceID a = *(const SDL_AudioDeviceID *)x, b = *(const SDL_AudioDeviceID *)y;
|
||||
const char *na = SDL_GetAudioDeviceName(a), *nb = SDL_GetAudioDeviceName(b);
|
||||
int c = SDL_strcasecmp(na ? na : "", nb ? nb : "");
|
||||
return c ? c : a < b ? -1 : a > b;
|
||||
}
|
||||
static SDL_AudioDeviceID *playback_devices(int *count) {
|
||||
SDL_AudioDeviceID *ids = SDL_GetAudioPlaybackDevices(count);
|
||||
if (!ids) *count = 0;
|
||||
else qsort(ids, (size_t)*count, sizeof *ids, device_order);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// One line describing device `id` ("name, 2 ch, 48000 Hz"), without a leading index.
|
||||
static void describe_device(char *buf, size_t n, SDL_AudioDeviceID id) {
|
||||
SDL_AudioSpec spec = {0};
|
||||
const char *name = SDL_GetAudioDeviceName(id);
|
||||
if (SDL_GetAudioDeviceFormat(id, &spec, NULL)) snprintf(buf, n, "%s, %d ch, %d Hz", name ? name : "?", spec.channels, spec.freq);
|
||||
else snprintf(buf, n, "%s", name ? name : "?");
|
||||
}
|
||||
|
||||
static void log_playback_devices(void) {
|
||||
int count;
|
||||
SDL_AudioDeviceID *ids = playback_devices(&count);
|
||||
if (!count) rt_log("audio: SDL reports no playback devices");
|
||||
for (int i = 0; i < count; i++) {
|
||||
char line[256];
|
||||
describe_device(line, sizeof line, ids[i]);
|
||||
rt_log("audio: %d: %s", i + 1, line);
|
||||
}
|
||||
SDL_free(ids);
|
||||
}
|
||||
|
||||
// NX2NAVI_AUDIO_DEVICE `want` to the SDL device to open, with `how` saying which rule decided. default (or empty) gives
|
||||
// SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, which SDL keeps pointed at the system default when Windows changes it. An all-digit
|
||||
// value is a 1-based index into playback_devices; any other text is a case-insensitive substring of SDL_GetAudioDeviceName,
|
||||
// and the first device in list order that contains it wins. A value that matches nothing also gives the default device
|
||||
// (the game is never silent because of this setting); `say` then logs the playback devices. SDL audio must be initialised.
|
||||
static SDL_AudioDeviceID resolve_audio_device(const char *want, char *how, size_t how_n, int say) {
|
||||
if (!want || !*want || !SDL_strcasecmp(want, "default")) {
|
||||
snprintf(how, how_n, "NX2NAVI_AUDIO_DEVICE=default: the system default playback device");
|
||||
return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
|
||||
}
|
||||
int count, matches = 0;
|
||||
SDL_AudioDeviceID *ids = playback_devices(&count), pick = 0;
|
||||
if (want[strspn(want, "0123456789")] == 0) {
|
||||
long index = strtol(want, NULL, 10); // more digits than a long holds: out of range either way
|
||||
if (index >= 1 && index <= count) {
|
||||
pick = ids[index - 1];
|
||||
snprintf(how, how_n, "NX2NAVI_AUDIO_DEVICE=%s: index %ld in the device list", want, index);
|
||||
} else {
|
||||
snprintf(how, how_n, "NX2NAVI_AUDIO_DEVICE=%s: no device has that index (%d playback devices), using the system "
|
||||
"default", want, count);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char *name = SDL_GetAudioDeviceName(ids[i]);
|
||||
if (!name || !SDL_strcasestr(name, want)) continue;
|
||||
if (!pick) pick = ids[i];
|
||||
matches++;
|
||||
}
|
||||
if (pick)
|
||||
snprintf(how, how_n, "NX2NAVI_AUDIO_DEVICE=%s: the first of %d device name%s containing it", want, matches,
|
||||
matches == 1 ? "" : "s");
|
||||
else
|
||||
snprintf(how, how_n, "NX2NAVI_AUDIO_DEVICE=%s: no device name contains it, using the system default", want);
|
||||
}
|
||||
SDL_free(ids);
|
||||
if (!pick && say) {
|
||||
rt_log("audio: %s; playback devices:", how);
|
||||
log_playback_devices();
|
||||
}
|
||||
return pick ? pick : SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
|
||||
}
|
||||
|
||||
// --list-audio-devices (main.c): each playback device with its index, name, channels and frequency, the one the system
|
||||
// default points at, then what NX2NAVI_AUDIO_DEVICE would pick. Needs no game files. Returns the exit code.
|
||||
int list_audio_devices(void) {
|
||||
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
|
||||
fprintf(stderr, "SDL audio init failed: %s\n", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
int count;
|
||||
SDL_AudioDeviceID *ids = playback_devices(&count);
|
||||
const char *def = SDL_GetAudioDeviceName(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK);
|
||||
char defname[128] = "";
|
||||
if (def) snprintf(defname, sizeof defname, "%s", def);
|
||||
if (!count) printf("no playback devices\n");
|
||||
for (int i = 0; i < count; i++) {
|
||||
char line[256];
|
||||
const char *name = SDL_GetAudioDeviceName(ids[i]);
|
||||
describe_device(line, sizeof line, ids[i]);
|
||||
printf("%d: %s%s\n", i + 1, line, *defname && name && !strcmp(name, defname) ? " (default)" : "");
|
||||
}
|
||||
SDL_free(ids);
|
||||
char how[256];
|
||||
SDL_AudioDeviceID id = resolve_audio_device(cfg_value("NX2NAVI_AUDIO_DEVICE"), how, sizeof how, 0);
|
||||
const char *name = SDL_GetAudioDeviceName(id);
|
||||
printf("%s -> '%s'\n", how, name ? name : "?");
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// NX2NAVI_AUDIO_CHANNELS: the output channels of this run, decided once. stereo gives 2; 5.1 gives 6 (FL FR C LFE RL RR,
|
||||
// the device sink's order and SDL's); auto gives 6 when the default playback device has at least six channels (a 5.1
|
||||
// the device sink's order and SDL's); auto gives 6 when the chosen playback device (NX2NAVI_AUDIO_DEVICE; the system default unless set) has at least six channels (a 5.1
|
||||
// or 7.1 speaker setup in Windows; SDL places 5.1 on 7.1) and 2 otherwise. IAudioDevice reports the same count to the
|
||||
// game (GetActiveChannelCount), which picks its sink layout from it. NX2NAVI_AUDIO=none: 2, without touching SDL.
|
||||
static int output_channels(void) {
|
||||
@@ -128,9 +239,14 @@ static int output_channels(void) {
|
||||
else if (!strcmp(want, "stereo")) n = 2;
|
||||
else {
|
||||
SDL_AudioSpec dev = {0};
|
||||
n = SDL_InitSubSystem(SDL_INIT_AUDIO) && SDL_GetAudioDeviceFormat(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &dev, NULL) &&
|
||||
dev.channels >= 6 ? 6 : 2;
|
||||
rt_log("audio: NX2NAVI_AUDIO_CHANNELS=auto: the default device has %d channels, output %s", dev.channels,
|
||||
char how[256];
|
||||
n = 2;
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO)) {
|
||||
// The choice output_open makes again (only that one logs the playback devices when nothing matches).
|
||||
SDL_AudioDeviceID id = resolve_audio_device(cfg_value("NX2NAVI_AUDIO_DEVICE"), how, sizeof how, 0);
|
||||
if (SDL_GetAudioDeviceFormat(id, &dev, NULL) && dev.channels >= 6) n = 6;
|
||||
}
|
||||
rt_log("audio: NX2NAVI_AUDIO_CHANNELS=auto: the chosen device has %d channels, output %s", dev.channels,
|
||||
n == 6 ? "5.1" : "stereo");
|
||||
}
|
||||
int expected = 0;
|
||||
@@ -218,6 +334,25 @@ static void SDLCALL sdl_pull(void *user, SDL_AudioStream *stream, int additional
|
||||
}
|
||||
}
|
||||
|
||||
static void output_timing(Mixer *m, int rate);
|
||||
|
||||
// Opens an SDL stream on `dev` and starts it. Returns 0 with the reason in `err` otherwise (no stream is left behind).
|
||||
static int stream_open(Mixer *m, int rate, SDL_AudioDeviceID dev, char *err, size_t err_n) {
|
||||
SDL_AudioSpec spec = {SDL_AUDIO_F32, m->channels, rate};
|
||||
m->stream = SDL_OpenAudioDeviceStream(dev, &spec, sdl_pull, m);
|
||||
if (!m->stream) {
|
||||
snprintf(err, err_n, "opening the device failed: %s", SDL_GetError());
|
||||
return 0;
|
||||
}
|
||||
if (!SDL_ResumeAudioStreamDevice(m->stream)) {
|
||||
snprintf(err, err_n, "starting the output failed: %s", SDL_GetError());
|
||||
SDL_DestroyAudioStream(m->stream);
|
||||
m->stream = NULL;
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void output_open(Mixer *m, int rate) {
|
||||
const char *mode = cfg_get("NX2NAVI_AUDIO");
|
||||
if ((mode && !strcmp(mode, "none")) || cfg_get("NX2NAVI_AUDIO_OFF")) {
|
||||
@@ -228,19 +363,31 @@ static void output_open(Mixer *m, int rate) {
|
||||
rt_log("audio: SDL audio init failed (%s): mixing without sound", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
SDL_AudioSpec spec = {SDL_AUDIO_F32, m->channels, rate};
|
||||
m->stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, sdl_pull, m);
|
||||
if (!m->stream) {
|
||||
rt_log("audio: no output device (%s): mixing without sound", SDL_GetError());
|
||||
return;
|
||||
char how[256], err[256];
|
||||
SDL_AudioDeviceID dev = resolve_audio_device(cfg_value("NX2NAVI_AUDIO_DEVICE"), how, sizeof how, 1);
|
||||
const char *chosen = dev == SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK ? NULL : SDL_GetAudioDeviceName(dev);
|
||||
if (chosen) snprintf(m->device_name, sizeof m->device_name, "%s", chosen);
|
||||
int ok = stream_open(m, rate, dev, err, sizeof err);
|
||||
if (!ok && chosen) { // the chosen device would not open: the default device, so the game still has sound
|
||||
rt_log("audio: %s: '%s' %s; using the system default, playback devices:", how, m->device_name, err);
|
||||
log_playback_devices();
|
||||
dev = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
|
||||
m->device_name[0] = 0;
|
||||
ok = stream_open(m, rate, dev, err, sizeof err);
|
||||
} else {
|
||||
rt_log("audio: %s", how);
|
||||
}
|
||||
if (!SDL_ResumeAudioStreamDevice(m->stream)) {
|
||||
rt_log("audio: starting the output failed (%s): mixing without sound", SDL_GetError());
|
||||
SDL_DestroyAudioStream(m->stream);
|
||||
m->stream = NULL;
|
||||
if (!ok) {
|
||||
rt_log("audio: no output device (%s): mixing without sound", err);
|
||||
return;
|
||||
}
|
||||
m->device_ok = 1;
|
||||
m->device = dev == SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK ? 0 : dev;
|
||||
output_timing(m, rate);
|
||||
}
|
||||
|
||||
// After stream_open: the device's period, the output target that follows from it, and the stall limit. Logs the device.
|
||||
static void output_timing(Mixer *m, int rate) {
|
||||
SDL_AudioSpec dev = {0};
|
||||
int period = 0;
|
||||
if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(m->stream), &dev, &period) || period <= 0) period = m->frame;
|
||||
@@ -255,11 +402,40 @@ static void output_open(Mixer *m, int rate) {
|
||||
dev.channels, m->prefill * 5, period, m->target * 1000.0 / rate);
|
||||
}
|
||||
|
||||
// Render thread, from audio_mixer_pace_ns while the device takes nothing. An explicitly chosen device (NX2NAVI_AUDIO_DEVICE)
|
||||
// can disappear (a virtual sink removed, a USB headset unplugged); SDL keeps only the system default device alive across
|
||||
// that, so the stream of a chosen one just stops being pulled, and nothing in this file polls SDL's event queue for the
|
||||
// removal. This looks once a second whether SDL still knows the device and, if not, moves the output to the system
|
||||
// default; if that fails too, the mixer goes on without a device, as with NX2NAVI_AUDIO=none. Destroying the stream waits
|
||||
// for the SDL callback (sdl_pull), which takes no lock the render thread holds.
|
||||
static void device_check(Mixer *m, uint64_t now) {
|
||||
if (now - m->watch_ns < 1000000000ull) return;
|
||||
m->watch_ns = now;
|
||||
if (SDL_GetAudioDeviceName(m->device)) return;
|
||||
rt_log("audio: device '%s' is gone; moving the output to the system default device", m->device_name);
|
||||
SDL_DestroyAudioStream(m->stream);
|
||||
m->stream = NULL;
|
||||
m->device = 0;
|
||||
m->device_name[0] = 0;
|
||||
m->ready = 0;
|
||||
atomic_store_explicit(&m->pulled_ns, 0, memory_order_release);
|
||||
char err[256];
|
||||
if (stream_open(m, m->rate, SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, err, sizeof err)) {
|
||||
output_timing(m, m->rate);
|
||||
} else {
|
||||
m->device_ok = 0;
|
||||
rt_log("audio: no output device (%s): mixing without sound", err);
|
||||
}
|
||||
}
|
||||
|
||||
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now) {
|
||||
Mixer *m = r->mixer;
|
||||
if (!m || !m->device_ok || !atomic_load_explicit(&m->playing, memory_order_acquire)) return -1;
|
||||
uint64_t pulled = atomic_load_explicit(&m->pulled_ns, memory_order_acquire);
|
||||
if (!pulled || (now > pulled && now - pulled > m->stall_ns)) return -1;
|
||||
if (!pulled || (now > pulled && now - pulled > m->stall_ns)) {
|
||||
if (m->device) device_check(m, now);
|
||||
return -1;
|
||||
}
|
||||
uint64_t have = atomic_load(&m->wr) - atomic_load_explicit(&m->rd, memory_order_acquire);
|
||||
if (have < (uint64_t)m->target) return 0;
|
||||
int64_t ns = (int64_t)((have - (uint64_t)m->target + 1) * 1000000000ull / (uint64_t)m->rate);
|
||||
|
||||
@@ -32,7 +32,7 @@ static int save_ok = 1; // the last cfg_save_to wrote (o
|
||||
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
|
||||
static const char *const known[] = {
|
||||
"CORES", "TM_DISPATCH", "NVN2_CS_TABLE", "SKIP_SHADERS", "MOVIE_DEBAND", "MOVIE_FPS", "MOVIE_SKIP", "MOVIE_OVERLAY", "MOVIES", "NPLN", "NVN2_PROGRAMS", "NVN_TRAP", "OCARINA_REALTIME", "ONESHOT_WAIT_MS", "OCARINA_STEP", "VI_HZ", "VI_VRR", "TIME_DUMP", "SHADER_DUMP", "SHOT_AFTER", "SHOT_DIR", "SHOT_EVERY", // Switch 2 port: movie skip (romfs.c), NVN2 proc traps (nvn_procs.c)
|
||||
"AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CHANNELS", "AUDIO_GAME_PRIO", "AUDIO_OFF", "AUDIO_PREFILL_MS", "AUDIO_STATS",
|
||||
"AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CHANNELS", "AUDIO_DEVICE", "AUDIO_GAME_PRIO", "AUDIO_OFF", "AUDIO_PREFILL_MS", "AUDIO_STATS",
|
||||
"BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT",
|
||||
"BIND_LSTICK_RIGHT", "BIND_LSTICK_UP", "BIND_MINUS",
|
||||
"BIND_PLUS", "BIND_R", "BIND_RCLICK", "BIND_RIGHT", "BIND_RSTICK_DOWN", "BIND_RSTICK_LEFT", "BIND_RSTICK_RIGHT", "BIND_RSTICK_UP",
|
||||
@@ -90,7 +90,7 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"PCSS", "display"},
|
||||
{"MODS_DIR", "mods"}, {"MODS_ORDER", "mods"}, {"MODS_DISABLED", "mods"}, {"MODS_REBUILD", "mods"},
|
||||
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
|
||||
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"}, {"MENU_SOUND_VOLUME", "audio"},
|
||||
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"}, {"AUDIO_DEVICE", "audio"}, {"MENU_SOUND_VOLUME", "audio"},
|
||||
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"},
|
||||
{"PAD_LAYOUT", "input"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
|
||||
{"STICK_SENS", "input"},
|
||||
@@ -168,6 +168,7 @@ static const CfgMeta metas[] = {
|
||||
{"ASSUME_FOCUS", "1", "0|1", LIVE}, // read on every focus change (ov_system.c)
|
||||
// Sound, Controls, Menu
|
||||
{"VOLUME", "100", "0..100", LIVE}, {"MENU_SOUND_VOLUME", "80", "0..100", LIVE}, {"AUDIO_CHANNELS", "stereo", "auto|stereo|5.1", RESTART},
|
||||
{"AUDIO_DEVICE", "default", NULL, RESTART}, // default|<1-based index>|<part of a device name> (audio_mixer.c resolve_audio_device)
|
||||
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
|
||||
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "nintendo", "auto|nintendo|xbox|playstation", LIVE},
|
||||
{"PROMPTS", "nintendo", "nintendo|xbox|playstation|pc", RESTART},
|
||||
|
||||
@@ -155,6 +155,8 @@ int probe_cfg(void) {
|
||||
CHECK(m && EQ(m->def, "130") && EQ(m->values, "0..300"), "meta: rumble strength 130 in 0..300");
|
||||
m = cfg_meta("PERF_OVERLAY");
|
||||
CHECK(m && EQ(m->def, "off") && EQ(m->values, "off|fps|full") && m->live, "meta: perf_overlay is live, off by default");
|
||||
m = cfg_meta("AUDIO_DEVICE");
|
||||
CHECK(m && EQ(m->def, "default") && m->values == NULL && !m->live, "meta: audio_device is free text, default, needs a restart");
|
||||
CHECK(cfg_meta("BIND_A") == NULL && cfg_meta("FPS_COUNTER") == NULL && cfg_meta("NO_SUCH") == NULL,
|
||||
"meta: keys the menu does not show are NULL");
|
||||
|
||||
|
||||
@@ -347,6 +347,7 @@ void rt_boot(void); // boot.c
|
||||
// Modes of nx2navi.exe. main.c has run mem_init and parsed the paths; each mode calls ldr_load itself if it needs the
|
||||
// modules, registers its host functions, then seals (hostfn_seal, or implicitly at the first rt_call). Return the
|
||||
// process exit code.
|
||||
int list_audio_devices(void); // audio_mixer.c: --list-audio-devices (SDL3 on every window back end)
|
||||
int probe_cfg(void); // cfg_probe.c
|
||||
int probe_dpi(void); // dpi_probe_win.c (Windows only)
|
||||
int probe_mods(void); // mods_probe.c
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// nx2navi.exe entry: reserves the guest address space, parses the command line and runs one mode.
|
||||
//
|
||||
// nx2navi.exe [--exefs DIR] [--romfs DIR] [--profile DIR] <mode>
|
||||
// nx2navi.exe [--exefs DIR] [--romfs DIR] [--profile DIR] [--audio-device VALUE] <mode>
|
||||
// --audio-device VALUE play the sound on one output device: sets NX2NAVI_AUDIO_DEVICE before the settings are read, so
|
||||
// it wins over nx2navi.ini's `audio_device =`. VALUE is default (the system default device),
|
||||
// a 1-based index as --list-audio-devices prints it (devices sorted by name), or part of a device name, case-insensitive
|
||||
// --list-audio-devices print each playback device's index, name, channels and frequency, mark the system default and
|
||||
// show which device NX2NAVI_AUDIO_DEVICE picks; needs no game files
|
||||
// --selftest layout, dispatch table and host-function registry; needs no game files
|
||||
// --probe-mods the mods folder scan, order and switches (mods.c); needs no game files
|
||||
// --probe-loader load and relocate the modules, check them against the measured 1.2.1 values
|
||||
@@ -475,7 +480,7 @@ static int probe_crash(int null) {
|
||||
}
|
||||
|
||||
static int usage(const char *argv0) {
|
||||
fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--selftest | --probe-loader | "
|
||||
fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--audio-device VALUE] [--list-audio-devices | --selftest | --probe-loader | "
|
||||
"--probe-cfg | --probe-dpi | --probe-mods | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
|
||||
"--probe-nvn | --probe-nvn-tex | --probe-window | --probe-shader | --probe-vkmem | "
|
||||
"--probe-vkexec | --probe-astc | --probe-movie | --probe-crash-call | --probe-crash-null | --probe-discord | "
|
||||
@@ -554,6 +559,12 @@ int main(int argc, char **argv) {
|
||||
if (path) {
|
||||
if (++i == argc) return usage(argv[0]);
|
||||
*path = argv[i];
|
||||
} else if (!strcmp(argv[i], "--audio-device")) {
|
||||
// Same approach as every setting: NX2NAVI_AUDIO_DEVICE is read from the environment first (cfg.c raw_get), and
|
||||
// nothing has read it yet because the audio renderer opens in rt_boot. An empty value would remove the variable
|
||||
// (plat_setenv) and hand the choice back to nx2navi.ini, so "default" is the spelling for the system default.
|
||||
if (++i == argc || !*argv[i]) return usage(argv[0]);
|
||||
plat_setenv("NX2NAVI_AUDIO_DEVICE", argv[i]);
|
||||
} else if (!strcmp(argv[i], "--seconds")) {
|
||||
char *end;
|
||||
if (++i == argc) return usage(argv[0]);
|
||||
@@ -579,6 +590,7 @@ int main(int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
if (!strcmp(mode, "--selftest")) return selftest();
|
||||
if (!strcmp(mode, "--list-audio-devices")) return list_audio_devices();
|
||||
if (!strcmp(mode, "--probe-loader")) return probe_loader();
|
||||
if (!strcmp(mode, "--probe-crash-call")) return probe_crash(0);
|
||||
if (!strcmp(mode, "--probe-crash-null")) return probe_crash(1);
|
||||
|
||||
Reference in New Issue
Block a user