Advance read each port's buffer when it completed the grain. Crash
Bandicoot 4 (PPSA02433) runs Wwise, which sets each object port's buffer
after mixing and clears the buffer for the next frame before it calls
Advance, so every grain was read as silence and the menu and gameplay had
no sound. A grain given by a set now plays the copy taken at that set; a
repeated buffer is still read at Advance.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A port without a new buffer for a grain repeated its last one whenever the
buffer had not moved on, which also covered a port given the same buffer
every grain. Crash Bandicoot 4 (PPSA02433) gives each Wwise object port the
same buffer every grain and none for about 350 ms while it loads; the
repeat then played the memory Wwise had reused, a loud burst of noise.
Only a port given a buffer once since it was created or cleared repeats it
now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 volume, mute and focus state that libSceAudioOut applies and libSceVideoOut changes now lives in libScePad, which both libraries already link, instead of in libSceAudioOut. The tests that compile the state directly and the environment reference follow the file.
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.
DeviceCallback applies the context's ramp to the stereo float it just filled, which is the only place the AudioOut2 main mix reaches the speakers; the title's own volumes and the mastering stay before it. At the default volume the ramp is at unity and the call returns without touching the stream. The controller sound card keeps its own path and is not touched.
Every non-vibration port goes through HostGainFilter just before SDL_QueueAudio, after the title's volume and channel order. With the default volume the filter returns the title's buffer untouched. Vibration ports go to the controller's sound card and are not touched.
The state sits behind a mutex and publishes its gain through an atomic, so the audio threads read it without locking. HostVolumeConfigure replaces the state, parsing ANYPS5_VOLUME and ANYPS5_MUTE_ON_FOCUS_LOSS before changing anything, so an invalid value leaves it untouched and throws an error naming the variable.
A vibration port plays through the controller's own sound card, such as
a DualSense over Bluetooth. When the backend disables that device, SDL
keeps the port's device disabled until it is closed (see the default
output fix), so the port's haptic audio stopped for the rest of the run.
Before a vibration port's device is used, close it and open the
controller sound card again when its status is stopped. A failed reopen
leaves the port with no device, so its output runs as a virtual port,
as a vibration port opened with no controller card does today. Such a
port is not retried later.
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]>
The AudioOut2 mixer queues the pad mix on the controller sound card unscaled. Pass the rumble percentage to the actuator write so 0 plays silence and the default is unchanged. The fallback that turns the same mix into motor rumble when no sound card plays it is scaled once, later, by the pad output.
The legacy AudioOut vibration port queues its PCM on the controller sound card unscaled. Pass the rumble percentage to the actuator write so 0 plays silence and the default is unchanged.
Vibration ports that play on a controller sound card write their PCM to the actuator channels with no way to reduce it. Add a trailing percentage parameter, defaulting to 100 which writes the same samples as before, that scales only the two actuator channels and writes silence at 0. The controller speaker channels are never scaled.
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]>