Two settings the owner asked for: "a devices tab to the bottom right of the video + audio tab in settings, when you click this you go to a page that lets you select specific audio output/input device, monitor, etc", and "2 seperate volume sliders for effects and announcer". No gameplay, network or file format change; quark.cfg gains six keys (older configs load as before: every new key starts at its default).
EFFECTS VOLUME and ANNOUNCER VOLUME (src/client/audio.{h,cpp}, src/client/game.cpp)
- Two sliders under VOLUME in VIDEO + AUDIO → SOUND, 0 to 1, default 1.00 (the sound as before), right-click: the default. quark.cfg keys
volume_effectsandvolume_announcer. - On top of the master: the mixer multiplies each voice by its category's volume (an effect: everything before the announcer's ids, the four hitsound tiers and the kill sound included; the announcer: its recorded / generated lines), then the master VOLUME and the soft limiter as before.
- Applied per mix period, read once a period from two atomics (no lock, nothing per sample but the multiply the gains already had): a sound already playing follows the slider from the next period (5 ms), and a 3D sound's per-period gain ramp is scaled with it, so a change makes no click.
- The tab still fits: the two sliders took 80 px the SOUND column didn't have (the announcer's VOICE section ended at 562 of the 640 footer), so the VOICE header went and ANNOUNCER / VOICE sit at the end of SOUND. The right column now ends at 598, the left at 620; nothing overlaps at 1280x720, 1920x1080 or 1024x768.
- Tests (assets, no sound device needed): a rifle shot at effects 1 / 0.5 / 0 (half within the limiter's bend, 0 silent), the announcer at 0 leaves it untouched; a line at announcer 0 is silent, effects at 0 leaves it untouched; a shot already playing goes silent from the next period when effects goes to 0.
DEVICES (src/client/game.cpp drawDeviceSettings, src/client/devices.h)
- A page of the VIDEO + AUDIO tab (
settings/video/devices, the tab stays lit), opened by DEVICES at the bottom right of the tab's footer, where the other tabs have their reset; BACK (bottom left) and Esc go back to VIDEO + AUDIO. - Rows like the FONT page: the default first, then the OS's devices by name; the one in use "> NAME <", a click on another picks it, saves quark.cfg and applies it at once. A list longer than its rows scrolls with the wheel over it (5 rows for the output and the monitor, 4 for the microphone). Names are upper case, and two devices with the same name are told apart ("DELL P2419H #2").
- When the OS is asked: when the page opens, on its REFRESH, and (monitors) when one is plugged in or out; never per frame. Windows: one MMDevice enumeration per list on a short-lived thread with its own COM apartment (whatever the game thread's is), a few ms.
- Stable ids in quark.cfg:
audio_output/audio_input(Windows: the endpoint id, which survives restarts and port changes; Linux: the ALSA device name; macOS: the CoreAudio UID),monitor(GLFW's monitor name, "#2" for a second of the same),fullscreen_hz. "" (or 0) = the default. A value is one line of at most 256 bytes (cut on a UTF-8 boundary). - A saved device that's gone (unplugged, renamed): the default is used and a notice says so ("THE CHOSEN AUDIO OUTPUT IS GONE: USING THE SYSTEM DEFAULT", "MONITOR ... NOT FOUND: USING THE PRIMARY"); the page says it in orange under the list, and the choice is kept for when it's back. REFRESH tries a missing output again.
- Tests (assets): a saved id found / the default / gone, same-named monitors numbered, ids as quark.cfg values (line breaks dropped, the 256-byte cut never splits a character), the OS lists asked for (and every entry with an id and a name), choosing an output before the device opens. UI test: every row (the one in use INERT, the rest CFG; a machine with no sound devices has only the default row, in use: INERT), REFRESH (
state("devscan")), BACK, Esc, in the menus and in game.
AUDIO OUTPUT (src/client/audio.cpp)
- Switching reopens the output the way a lost device or EXCLUSIVE AUDIO already does: Audio::setOutputDevice stores the id and sets the backend's reopen flag; its own loop closes the stream between two periods and opens the new device with exactly the same code (WASAPI shared at the engine's period, or exclusive at the device's; the same one-period queue). So nothing is added to the latency, and EXCLUSIVE AUDIO works on the chosen device too. Picking the device already open does nothing.
- Windows: the endpoint is opened by id (IMMDeviceEnumerator::GetDevice), only when it's active and activates; else the default endpoint, with the "gone" flag. The log line says which ("the chosen device" / "the default device (the chosen one is gone)"). A device unplugged mid-game falls back to the default as before (the session fails and reopens; the chosen one isn't there). The WinMM fallback (no WASAPI) stays on the default.
- Linux: ALSA's device hints (snd_device_name_hint, loaded at runtime like the output); offered: each card's sysdefault and HDMI entries, pulse, pipewire and jack (not every plugin route: front, surround51, dmix...). The output thread reopens between two writes.
- macOS: CoreAudio's devices with output (or input) streams, by UID and name (CoreAudio.framework now linked); the AudioQueue is made on the chosen device (kAudioQueueProperty_CurrentDevice), closed and made again on a change.
AUDIO INPUT
- Listed and stored like the output (
audio_input), but nothing in the game records sound yet (no voice chat), and the page's hint says so: the choice is kept for voice chat later.
MONITOR and REFRESH RATE (src/client/game.cpp applyDisplayMode)
- MONITOR: PRIMARY MONITOR (the default, as before), then each monitor with its desktop mode ("GENERIC PNP MONITOR 2560X1440 240 HZ"). Borderless covers the chosen monitor, exclusive fullscreen takes it, windowed opens 100 px in from its corner. Picking one in fullscreen / borderless moves the window there at once.
- REFRESH RATE (exclusive fullscreen only): DESKTOP (the monitor's own rate, as before) or any other rate the monitor has at its desktop size, highest first; GLFW takes the closest mode. A rate saved for another monitor is shown as "(NOT ON THIS MONITOR)" until changed.
- The desktop mode is remembered while the game is exclusive on a monitor: glfwGetVideoMode then reports the game's own mode, so going back to DESKTOP (or to borderless) used the switched rate.
- gameMonitor (Latent Sync, F3, the FPS LIMIT default) uses the chosen monitor when not exclusive, from a pointer looked up only when the setting or the monitors change (GLFW's monitor callback sets a flag the frame reads; a gone monitor's pointer is never used again).
- The UI test puts
monitorandfullscreen_hzback at once likedisplay_mode(they move the window).
Docs
- docs/RELEASE_CHECKLIST.md: rows for the two volumes and the DEVICES page. docs/UI_TEST.md: the page, its ids and expectations. assets/provenance/ui.json:
ui/settings/video/devices(AI-made).