From 84bd8459e9501d41768f823d19847c51e9c38268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 10 Sep 2026 12:56:15 -0600 Subject: [PATCH] sceAudio: implement sceAudioOneshotOutput, and cover the rest of the channel rules Another pass looking for gaps, all of it now recorded by audio/blocking/channels and audio/blocking/oneshot. sceAudioOneshotOutput was the last unimplemented entry in the module, returning "library not linked" to anyone who called it. It plays one buffer on a channel it never reserves, so the channel frees itself when the buffer runs out, and its argument checks are their own set: any positive sample count, aligned or not, no upper bound, a negative volume rejected rather than skipped, and no busy check at all. No game is known to use it; it is implemented because tracing it turned out to be cheap, not because anything needed it. The channels test confirms four behaviors: sceAudioChReserve(-1) skips a released channel that is still playing while a reserve of it succeeds, a second channel joining a running mixer doesn't lose a block unlike a first, mono counts down in the same 64-sample steps over the same time as stereo, and the panned blocking output has no extra delay on the high channels. Also: the SRC resampler now interpolates into the next buffer at a buffer join instead of holding the last sample, since the codec reads the two descriptors as one stream. That only shows up at non-native rates and no test can see it. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/FunctionWrappers.h | 5 +++++ Core/HLE/__sceAudio.cpp | 42 ++++++++++++++++++++++++++++++------- Core/HLE/__sceAudio.h | 3 +++ Core/HLE/sceAudio.cpp | 33 +++++++++++++++++++++++++++-- docs/sceAudio.md | 12 +++++++++++ pspautotests | 2 +- test.py | 2 ++ 7 files changed, 89 insertions(+), 10 deletions(-) diff --git a/Core/HLE/FunctionWrappers.h b/Core/HLE/FunctionWrappers.h index 926ed29f72..fc7765fd23 100644 --- a/Core/HLE/FunctionWrappers.h +++ b/Core/HLE/FunctionWrappers.h @@ -307,6 +307,11 @@ template void WrapU_UIIIII() { RETURN(retval); } +template void WrapU_IIIIIU() { + u32 retval = func(PARAM(0), PARAM(1), PARAM(2), PARAM(3), PARAM(4), PARAM(5)); + RETURN(retval); +} + template void WrapU_UIIIU() { u32 retval = func(PARAM(0), PARAM(1), PARAM(2), PARAM(3), PARAM(4)); RETURN(retval); diff --git a/Core/HLE/__sceAudio.cpp b/Core/HLE/__sceAudio.cpp index 584fb3dbcd..78b4a7ab85 100644 --- a/Core/HLE/__sceAudio.cpp +++ b/Core/HLE/__sceAudio.cpp @@ -298,6 +298,22 @@ u32 __AudioEnqueue(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol) return chan.sampleCount; } +void __AudioEnqueueOneshot(AudioChannel &chan, u32 samplePtr, u32 sampleCount, u32 format, int leftVol, int rightVol) { + // No reservation and no busy check - handing a channel a second one while the first is + // still playing simply replaces it. sampleCount deliberately stays zero on the channel, + // which is what lets it go back to being free once the buffer runs out. + chan.format = format; + chan.leftVolume = leftVol; + chan.rightVolume = rightVol; + chan.remainingSamples = sampleCount; + + const bool startsDMA = samplePtr != 0 && !__AudioAnyChannelPlaying(); + chan.sampleAddress = samplePtr; + if (startsDMA) { + __AudioStartMixerDMA(); + } +} + u32 __AudioEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol) { u32 result = __AudioEnqueue(chan, samplePtr, leftVol, rightVol); if (result != SCE_ERROR_AUDIO_CHANNEL_BUSY) { @@ -508,6 +524,8 @@ static bool __AudioMixSRC(AudioSRCChannel &chan) { // Zero means "whatever the output is running at", so no conversion. const int inRate = srcFrequency != 0 ? srcFrequency : mixFrequency; const u32 ratio = (u32)(((u64)(u32)inRate << 16) / (u32)mixFrequency); + // At the output rate the fraction never moves off zero, so there is nothing to interpolate. + const bool resampling = ratio != 0x10000; const bool mono = chan.format == PSP_AUDIO_FORMAT_MONO; const u32 stride = mono ? 2 : 4; const int leftVol = chan.leftVolume; @@ -523,16 +541,26 @@ static bool __AudioMixSRC(AudioSRCChannel &chan) { const AudioPendingBuffer &buf = chan.buffers[0]; const u32 addr = buf.address + chan.playedSamples * stride; - // Interpolating against the following sample matters when a game reserved 22050Hz - // or similar; at the native rate the fraction is always zero and this reduces to a - // plain copy. - const u32 avail = std::min(buf.samples - chan.playedSamples, 2u); - if (!chan.mute && Memory::IsValidRange(addr, avail * stride)) { + if (!chan.mute && Memory::IsValidRange(addr, stride)) { const s16_le *src = (const s16_le *)Memory::GetPointerUnchecked(addr); const int l0 = src[0]; const int r0 = mono ? l0 : src[1]; - const int l1 = avail > 1 ? (int)src[stride / 2] : l0; - const int r1 = avail > 1 ? (mono ? l1 : (int)src[stride / 2 + 1]) : r0; + int l1 = l0; + int r1 = r0; + if (resampling) { + // Interpolate against the sample after this one. At the end of a buffer that + // is the start of the next, since the codec reads the two descriptors as one + // unbroken stream - holding the last sample instead would tick at every join. + u32 nextAddr = addr + stride; + if (chan.playedSamples + 1 >= buf.samples) { + nextAddr = chan.bufferCount > 1 ? chan.buffers[1].address : 0; + } + if (nextAddr != 0 && Memory::IsValidRange(nextAddr, stride)) { + const s16_le *next = (const s16_le *)Memory::GetPointerUnchecked(nextAddr); + l1 = next[0]; + r1 = mono ? l1 : next[1]; + } + } // 15 bits of fraction, not 16 - a full 16 would overflow the product against a // full-scale difference. const int frac = (int)(chan.frac >> 1); diff --git a/Core/HLE/__sceAudio.h b/Core/HLE/__sceAudio.h index f39eb32e48..efe022b9ae 100644 --- a/Core/HLE/__sceAudio.h +++ b/Core/HLE/__sceAudio.h @@ -45,6 +45,9 @@ void __AudioSetSRCFrequency(int freq); // count, or SCE_ERROR_AUDIO_CHANNEL_BUSY when a buffer is already in flight. A negative // volume means "leave it alone". u32 __AudioEnqueue(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol); +// sceAudioOneshotOutput's variant: hands over a buffer without a reservation, so it carries its +// own sample count and format and never reports busy. +void __AudioEnqueueOneshot(AudioChannel &chan, u32 samplePtr, u32 sampleCount, u32 format, int leftVol, int rightVol); // Same, but parks the calling thread until the channel frees up. Only one thread can be // parked; a second one gets SCE_ERROR_AUDIO_CHANNEL_BUSY straight back. u32 __AudioEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol); diff --git a/Core/HLE/sceAudio.cpp b/Core/HLE/sceAudio.cpp index 44f1174d0b..660fec804e 100644 --- a/Core/HLE/sceAudio.cpp +++ b/Core/HLE/sceAudio.cpp @@ -330,6 +330,34 @@ static u32 sceAudioChangeChannelConfig(u32 chan, u32 format) { return hleLogDebug(Log::sceAudio, 0); } +// Plays one buffer on a channel without reserving it, so the channel frees itself once the +// buffer runs out. The checks are looser than sceAudioChReserve's - any positive sample count +// goes, aligned or not - and stricter on volume, where a negative one is an error rather than +// meaning "leave it alone". +static u32 sceAudioOneshotOutput(int chan, int sampleCount, int format, int leftvol, int rightvol, u32 samplePtr) { + if ((u32)leftvol > 0xFFFF || (u32)rightvol > 0xFFFF) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume"); + } + if (chan < 0) { + chan = GetFreeChannel(); + if (chan < 0) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_NO_CHANNELS_AVAILABLE, "no channels remaining"); + } + } else if (chan >= (int)PSP_AUDIO_CHANNEL_MAX || g_audioChans[chan].reserved) { + // A reserved channel is refused with the same error as one that doesn't exist. + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel %d", chan); + } + if (sampleCount <= 0) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_OUTPUT_SAMPLE_DATA_SIZE_NOT_ALIGNED, "invalid sample count"); + } + if (format != PSP_AUDIO_FORMAT_MONO && format != PSP_AUDIO_FORMAT_STEREO) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_FORMAT, "invalid format"); + } + + __AudioEnqueueOneshot(g_audioChans[chan], samplePtr, sampleCount, format, leftvol, rightvol); + return hleLogDebug(Log::sceAudio, chan); +} + static u32 sceAudioChangeChannelVolume(u32 chan, int leftvol, int rightvol) { if (leftvol > 0xFFFF || rightvol > 0xFFFF) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid chan %d volume %d %d", chan, leftvol, rightvol); @@ -595,8 +623,9 @@ const HLEFunction sceAudio[] = {0X5C37C0AE, &WrapU_V, "sceAudioSRCChRelease", 'x', "" }, {0XE0727056, &WrapU_UU, "sceAudioSRCOutputBlocking", 'x', "xx" }, - // Never seen these used - {0X41EFADE7, nullptr, "sceAudioOneshotOutput", '?', "" }, + {0X41EFADE7, &WrapU_IIIIIU, "sceAudioOneshotOutput", 'x', "iiiiix"}, + + // Never seen this used {0XB61595C0, nullptr, "sceAudioLoopbackTest", '?', "" }, // Microphone interface diff --git a/docs/sceAudio.md b/docs/sceAudio.md index c6ce676b91..a99c7598c1 100644 --- a/docs/sceAudio.md +++ b/docs/sceAudio.md @@ -150,6 +150,18 @@ checks is observable. Accepted SRC frequencies are 8000, 11025, 12000, 16000, 22050, 24000, 32000 and 48000, plus whatever the output is currently running at - which is how 44100 and 0 get through. +## sceAudioOneshotOutput + +The odd one out: it plays a single buffer on a channel it never reserves. The channel stays +free as far as everything else is concerned, so `sceAudioChRelease` on it answers `80260008`, +and it returns to the pool by itself once the buffer runs out. It returns the channel number, +takes any positive sample count - alignment and the 0xFFC0 ceiling do not apply - and unlike +the other outputs a negative volume is an error rather than "leave it alone". There is no busy +check either: a second one-shot while the first is playing just replaces it. + +No game is known to call it. It is implemented because the behavior turned out to be simple +once traced, not because anything needed it. + ## sceVaudio is a third shape `sceVaudio` reserves the same channel as Output2 and SRC, but its own release is not the same diff --git a/pspautotests b/pspautotests index 22c5bd9677..4eb50721ea 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 22c5bd96777d9d52396d10d37c7c59709623ad73 +Subproject commit 4eb50721ea35a57f786b4230d2d852896ddfb74b diff --git a/test.py b/test.py index 4aa6a6bd58..62a540448d 100755 --- a/test.py +++ b/test.py @@ -114,9 +114,11 @@ tests_good = [ "audio/mp3/setloopnum", "audio/mp3/stream", "audio/blocking/contend", + "audio/blocking/channels", "audio/blocking/depth", "audio/blocking/errors", "audio/blocking/overhead", + "audio/blocking/oneshot", "audio/blocking/restlen", "audio/blocking/vaudio", "audio/sceaudio/datalen",