The default open harness counted SDL_QueueAudio calls by target and no
longer required the device id, so a queue to the wrong device passed in
every mode. Keep the requirement; it is lifted only while the disconnect
mode has removed the default output, where the counts check the target.
No existing mode queues audio, so their results are unchanged.
Bring the branch up to the current upstream main. The only conflict was the
AudioOut debt doc, where both sides added a Functional entry; both are kept.
The output path now asks SDL for the status of a port's device. The port
lock and resampling harnesses link the shared library with fake device
IDs, and without a stub the status query reaches the real SDL, which
reports an unknown ID as stopped. The port then reopened its device and
the harnesses' expectations about the calls they see broke.
Both harnesses answer the query as playing, as the other AudioOut
harnesses that stub the device functions do.
libSceVideoOut reads and changes the shared host volume state, and linking libSceAudioOut for it puts a second shared library with its own static SDL2 into libSceVideoOut. Both libraries already link libScePad, so the headers that describe that state move there first; the include paths and guards follow.
Register audio_out_host_volume_state. Covers the unity default, each command publishing its gain, focus acting only with the option on, initial values, a refused configuration leaving the state alone, both variables read from the environment with the invalid one named, and readers never seeing a gain above unity while commands run.
Accepts 0 and 1 for the flag and 0 to 100 for the level; refuses empty, padded, signed, fractional, out-of-range and overflowing values with the variable named.
At unity the title's pointer comes back and nothing is copied, a changed gain works on a copy and leaves the title's buffer as it was, and the scratch buffer is processed in place.
Covers the byte-for-byte bypass at unity, the first sample and landing frame matching the 16-bit ramp, no step above the slope, a ramp split across buffers, and the absence of clamping.
Register audio_out_host_gain. Covers the byte-for-byte bypass at unity, the exact first sample and landing frame of a ramp, no step larger than the ramp slope, a ramp split across buffers equal to one buffer, saturation above unity, one gain per frame on every channel, a silent gain, and the return to bypass.
Covers the option off, a focus loss and gain with it on, the user's own mute and volume surviving a round trip, and the option changing while the window is unfocused.
The vibration harness gains a disconnect mode. It opens a vibration port
on the controller sound card, reports that card as stopped, and writes
one buffer. The output must open the card again, queue the PCM into the
new device and never into the stopped one, then close both devices with
the port.
On a tree without the reopen the mode fails at "the stopped controller
sound card was not reopened".
The default-open harness gains a disconnect mode. It opens the main port
on the default output, reports that device as stopped, and writes one
buffer. The output must open a replacement device, queue into it and
never queue into the stopped one, then close both devices with the port.
On a tree without the reopen the mode fails at "the removed default
output was not replaced".
SDL marks an opened device as disabled when the backend reports it lost
(SDL_OpenedAudioDeviceDisconnected in src/audio/SDL_audio.c of the
bundled 3rdparty/SDL2 at 4b69833bc). The flag is set back only when a
device is opened again, so a disabled device stays silent until it is
closed, and SDL_GetAudioDeviceStatus reports SDL_AUDIO_STOPPED.
AudioOut never checked that status for a port's default output, so
after the output went away the title's audio stayed silent for the rest
of its run while every output call still returned success.
Before a default-output port is used, reopen its device when its status
is stopped. A failed reopen leaves the port with no device, so it runs
as a virtual port, as a port opened with no output does today. Such a
port is not retried later.
A device-less context can run its queue model on a clock the test sets. With the clock frozen, a blocking push that fills the queue is seen still waiting with its grain queued and not counted, and returns once the test plays one grain; a blocking push that meets a queue filled by non-blocking pushes does not queue past the depth.
A blocking sceAudioOut2ContextPush waited only before queueing its grain, so the push that filled the queue returned at once. PRAGMATA's Wwise feeds its context from a thread that pushes and then wakes the renderer; the renderer sees a full queue, sleeps, and is woken again only by the game's next RenderAudio, so the mix advanced four grains per game frame. A blocking push now also waits after queueing until a grain finishes, bounded by the existing 200 ms limit.
An AudioOut2 context handle was the host pointer itself, and every
sceAudioOut2Context* call and sceAudioOut2PortCreate only rejected 0. A
second sceAudioOut2ContextDestroy of the same handle deleted the context
twice, Push, Advance, GetQueueLevel and SetAttributes on a destroyed
context used freed memory, and PortCreate accepted any nonzero value and
kept it as the port's context, so the next PCM attribute on that port read
the grain size through it.
Live contexts are now kept in a set: ContextCreate adds the context,
ContextDestroy removes it before releasing it, and the other context calls
and PortCreate return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE for a handle that
is not in the set. The queue level test that passed a stack object as a
handle now uses a created context with its device closed.
AI-assisted: yes
CreateContext() reads the variable, so a value exported in the developer's shell reached the AudioOut2 tests: garbage aborted seven of the sixty audio tests and -15 broke the unity fold in audio_out2_port_layouts. Every test that creates a context now unsets the variable at the start of main(), and the tests that exercise it set it themselves.
Throws() now takes the exception type it expects, so the two port checks still require std::invalid_argument while the environment check requires std::runtime_error. The rejection error keeps the original pointer to the value, so a value with leading whitespace is reported verbatim and the test checks the name=value prefix.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
Check on the quad and pulse layouts that 100 and above write the same bytes as the call without a percentage, that 0 silences exactly the actuator channels, that 50 halves them, and that the speaker and unmapped channels are never touched.
A set-but-invalid value, including an empty one, a non-finite number, a
number outside -60 to 0 or trailing text, stops the title with an error
naming the variable and the value instead of silently keeping 0 dB. The
gain is parsed once when the AudioOut2 context is created, kept in the
context and passed to the mix loop, so the audio thread never reads the
environment.
The one-read rule is pinned end to end: a play that changes the variable
to a value that would stop the title after the context was created keeps
the parsed fold and never re-reads the environment.
Add APS5_PAD_SPEAKER_FALLBACK_DB for AudioOut2 controller-speaker ports
folded into the main mix when no controller speaker is present. The
default 0 dB keeps the existing unity mix; mute removes the fallback
copy so a title's own main-port copy is heard instead.
#9838 read sceAudioOut2SetSystemDebugState in PS5 firmware 3.20 and 4.03:
it returns 0x80268006 (not initialized) before sceAudioOut2Initialize,
ahead of its pointer, id and size checks. The debt entry for
sceAudioOut2EnableChat and sceAudioOut2DisableChat records the same
0x80268006 before AudioOut2 initialization. Both were left out while
AudioOut2 initialization was not tracked; AudioOut2IsInitialized_nid_no_patch
now tracks it, so all three return 0x80268006 first and change no state.
The two debt entries drop the closed gap.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PS5 module read for #9027 returns 0x80268006 from sceAudioOut2MasteringInit,
sceAudioOut2MasteringInitEx, sceAudioOut2MasteringTerm,
sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam before
sceAudioOut2Initialize. #9027 left the check out because AudioOut2
initialization was not tracked; AudioOut2IsInitialized_nid_no_patch now
tracks it, so each of these returns 0x80268006 first, before its own
mastering-state and argument checks, and changes no state. The debt entry
drops the closed gap.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On PS5 firmware 13.60 a second sceAudioOut2Initialize returns 0x80268004,
also after sceAudioOutInit, and a second sceAudioOutInit returns
0x8026000e. sceAudioOutInit after sceAudioOut2Initialize still returns 0.
sceAudioOut2InitializeEx and sceAudioOut2LoInit keep returning 0; their
repeats were not measured.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SDL's WASAPI enumeration drops failures silently: EnumAudioEndpoints or
GetCount errors leave the list empty, and a device whose metadata lookup
fails is left out, so an empty list does not prove there is no default
endpoint. The skip now also requires IMMDeviceEnumerator's
GetDefaultAudioEndpoint(eRender, eConsole), the call SDL's default open
makes, to return ERROR_NOT_FOUND. The probe runs only when the wasapi list
is empty.
audio_out_default_open gains wasapi_zero_default (empty WASAPI list, the
probe finds an endpoint, the default is opened) and checks that the probe
runs only for an empty WASAPI list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>