Merge pull request #22279 from hrydgard/audio-blocking-hardware-model

sceAudio: model the real buffering, so contending threads get told BUSY
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-09-10 17:23:28 -06:00
commit 2e6fd06ed6
11 files changed
+1067 -440

No files matched your search

+1
View File
@@ -19,6 +19,7 @@ for it:
| [docs/pspautotests.md](docs/pspautotests.md) | Workflow for improving PPSSPP using pspautotests |
| [docs/pspautotests-hardware.md](docs/pspautotests-hardware.md) | Writing a new pspautotest, and running it on a real PSP over PSPLink to record its `.expected` |
| [docs/frametest.md](docs/frametest.md) | Framedump rendering tests |
| [docs/sceAudio.md](docs/sceAudio.md) | How the audio output calls block, how deep they buffer, and what each error means |
| [docs/WebSocketDebugger.md](docs/WebSocketDebugger.md) | WebSocket debugger protocol reference |
| [docs/reverse-engineering.md](docs/reverse-engineering.md) | Disassembling a firmware PRX with `--re-module`, to find out what the hardware actually does |
+5
View File
@@ -307,6 +307,11 @@ template<u32 func(u32, int , int , int, int, int)> void WrapU_UIIIII() {
RETURN(retval);
}
template<u32 func(int, int, int, int, int, u32)> void WrapU_IIIIIU() {
u32 retval = func(PARAM(0), PARAM(1), PARAM(2), PARAM(3), PARAM(4), PARAM(5));
RETURN(retval);
}
template<u32 func(u32, int , int , int, u32)> void WrapU_UIIIU() {
u32 retval = func(PARAM(0), PARAM(1), PARAM(2), PARAM(3), PARAM(4));
RETURN(retval);
+428 -233
View File
@@ -37,6 +37,7 @@
#include "Core/ELF/ParamSFO.h"
#include "Core/HLE/sceKernelTime.h"
#include "Core/HLE/ErrorCodes.h"
#include "Core/HLE/HLE.h"
#include "Core/HLE/__sceAudio.h"
#include "Core/HLE/sceAudio.h"
#include "Core/HLE/sceKernel.h"
@@ -47,10 +48,6 @@
// atomic locks are used on the lock. TODO: make this lock-free
std::atomic_flag atomicLock_;
// We copy samples as they are written into this simple ring buffer.
// Might try something more efficient later.
FixedSizeQueue<s16, 32768 * 8> chanSampleQueues[PSP_AUDIO_CHANNEL_MAX + 1];
int eventAudioUpdate = -1;
// TODO: This is now useless and should be removed. Just scared of breaking states.
@@ -72,11 +69,6 @@ WaveFileWriter g_wave_writer;
static bool m_logAudio;
#endif
// High and low watermarks, basically. For perfect emulation, the correct values are 0 and 1, respectively.
// TODO: Tweak. Hm, there aren't actually even used currently...
static int chanQueueMaxSizeFactor;
static int chanQueueMinSizeFactor;
static void hleAudioUpdate(u64 userdata, int cyclesLate) {
// Schedule the next cycle first. __AudioUpdate() may consume cycles.
CoreTiming::ScheduleEvent(audioIntervalCycles - cyclesLate, eventAudioUpdate, 0);
@@ -100,9 +92,6 @@ void __AudioInit() {
mixFrequency = 44100;
srcFrequency = 0;
chanQueueMaxSizeFactor = 2;
chanQueueMinSizeFactor = 1;
__AudioCPUMHzChange();
eventAudioUpdate = CoreTiming::RegisterEvent("AudioUpdate", &hleAudioUpdate);
@@ -110,10 +99,11 @@ void __AudioInit() {
CoreTiming::ScheduleEvent(audioIntervalCycles, eventAudioUpdate, 0);
CoreTiming::ScheduleEvent(audioHostIntervalCycles, eventHostAudioUpdate, 0);
for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX + 1; i++) {
for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX; i++) {
g_audioChans[i].index = i;
g_audioChans[i].clear();
}
g_audioSRC.clear();
mixBuffer = new s32[hwBlockSize * 2];
clampedMixBuffer = new s16[hwBlockSize * 2];
@@ -123,7 +113,7 @@ void __AudioInit() {
}
void __AudioDoState(PointerWrap &p) {
auto s = p.Section("sceAudio", 1, 2);
auto s = p.Section("sceAudio", 1, 3);
if (!s)
return;
@@ -141,33 +131,57 @@ void __AudioDoState(PointerWrap &p) {
mixFrequency = 44100;
}
if (s >= 2) {
// TODO: Next time we bump, get rid of this. It's kinda useless.
auto s = p.Section("resampler", 1);
if (p.mode == p.MODE_READ) {
System_AudioClear();
}
} else {
// Only to preserve the previous file format. Might cause a slight audio glitch on upgrades?
// Version 1 kept the whole mixed output queue here, and version 2 replaced it with an empty
// "resampler" section that never held anything. Version 3 drops that too. Either way the
// contents go in the bin: the backend is cleared and the game refills it within a block.
if (s == 2) {
auto resampler = p.Section("resampler", 1);
} else if (s < 2) {
FixedSizeQueue<s16, 512 * 16> outAudioQueue;
outAudioQueue.DoState(p);
}
if (p.mode == p.MODE_READ) {
System_AudioClear();
}
// Before v3 the SRC channel was a ninth entry in this array rather than its own thing, so
// older states carry one extra record here.
int chanCount = ARRAY_SIZE(g_audioChans);
Do(p, chanCount);
if (chanCount != ARRAY_SIZE(g_audioChans))
{
const int expected = s >= 3 ? (int)ARRAY_SIZE(g_audioChans) : (int)ARRAY_SIZE(g_audioChans) + 1;
if (chanCount != expected) {
ERROR_LOG(Log::sceAudio, "Savestate failure: different number of audio channels.");
p.SetError(p.ERROR_FAILURE);
return;
}
for (int i = 0; i < chanCount; ++i) {
g_audioChans[i].index = i;
g_audioChans[i].DoState(p);
if (i < (int)ARRAY_SIZE(g_audioChans)) {
g_audioChans[i].index = i;
g_audioChans[i].DoState(p);
} else {
// The ninth entry is the SRC channel in its old shape. Its queued audio is the old
// sample-ring format and can't be converted, but what reserve agreed on carries
// over - throwing that away is what leaves Output2 unreserved after the load.
AudioChannel old;
old.index = i;
old.DoState(p);
if (p.mode == p.MODE_READ) {
g_audioSRC.clear();
g_audioSRC.reserved = old.reserved;
g_audioSRC.sampleCount = old.sampleCount;
g_audioSRC.leftVolume = old.leftVolume;
g_audioSRC.rightVolume = old.rightVolume;
g_audioSRC.format = old.format;
}
}
}
if (s >= 3) {
g_audioSRC.DoState(p);
__AudioRoutingDoState(p);
}
// For older states the routing modes were read back once per channel, above.
__AudioCPUMHzChange();
}
@@ -176,10 +190,11 @@ void __AudioShutdown() {
delete [] clampedMixBuffer;
mixBuffer = 0;
for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX + 1; i++) {
for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX; i++) {
g_audioChans[i].index = i;
g_audioChans[i].clear();
}
g_audioSRC.clear();
#ifndef MOBILE_DEVICE
if (g_Config.bDumpAudio) {
@@ -188,132 +203,269 @@ void __AudioShutdown() {
#endif
}
u32 __AudioEnqueue(AudioChannel &chan, int chanNum, bool blocking) {
u32 ret = chan.sampleCount;
// The audio driver never copies a buffer on the way in. It stores the pointer, and its mixer
// thread reads 64 samples straight out of the game's memory every DMA block, walking
// sampleAddress forward until the buffer is spent. Everything below follows that shape; see
// docs/sceAudio.md for the behavior this is modelled on.
if (chan.sampleAddress == 0) {
// For some reason, multichannel audio lies and returns the sample count here.
if (chanNum == PSP_AUDIO_CHANNEL_SRC || chanNum == PSP_AUDIO_CHANNEL_OUTPUT2) {
ret = 0;
}
}
// If there's anything on the queue at all, it should be busy, but we try to be a bit lax.
//if (chanSampleQueues[chanNum].size() > chan.sampleCount * 2 * chanQueueMaxSizeFactor || chan.sampleAddress == 0) {
if (chanSampleQueues[chanNum].size() > 0) {
if (blocking) {
// TODO: Regular multichannel audio seems to block for 64 samples less? Or enqueue the first 64 sync?
int blockSamples = (int)chanSampleQueues[chanNum].size() / 2 / chanQueueMinSizeFactor;
if (__KernelIsDispatchEnabled()) {
AudioChannelWaitInfo waitInfo = {__KernelGetCurThread(), blockSamples};
chan.waitingThreads.push_back(waitInfo);
// Also remember the value to return in the waitValue.
__KernelWaitCurThread(WAITTYPE_AUDIOCHANNEL, (SceUID)chanNum + 1, ret, 0, false, "blocking audio");
} else {
// TODO: Maybe we shouldn't take this audio after all?
ret = SCE_KERNEL_ERROR_CAN_NOT_WAIT;
}
// Fall through to the sample queueing, don't want to lose the samples even though
// we're getting full. The PSP would enqueue after blocking.
} else {
// Non-blocking doesn't even enqueue, but it's not commonly used.
return SCE_ERROR_AUDIO_CHANNEL_BUSY;
}
}
if (chan.sampleAddress == 0) {
return ret;
}
// NOTE: The below is WRONG! See issue #20095.
//
// What we should be queueing here is just the sampleAddress and sampleCount. Then when dequeuing is when we should
// read the actual data.
int leftVol = chan.leftVolume;
int rightVol = chan.rightVolume;
if (leftVol == (1 << 15) && rightVol == (1 << 15) && chan.format == PSP_AUDIO_FORMAT_STEREO && IS_LITTLE_ENDIAN) {
// TODO: Add mono->stereo conversion to this path.
// Good news: the volume (1 << 15), specifically, doesn't affect the values at all.
// We can just do a direct memory copy.
const u32 totalSamples = chan.sampleCount * (chan.format == PSP_AUDIO_FORMAT_STEREO ? 2 : 1);
s16 *buf1 = 0, *buf2 = 0;
size_t sz1, sz2;
chanSampleQueues[chanNum].pushPointers(totalSamples, &buf1, &sz1, &buf2, &sz2);
if (Memory::IsValidAddress(chan.sampleAddress + (totalSamples - 1) * sizeof(s16_le))) {
Memory::Memcpy(buf1, chan.sampleAddress, (u32)sz1 * sizeof(s16));
if (buf2)
Memory::Memcpy(buf2, chan.sampleAddress + (u32)sz1 * sizeof(s16), (u32)sz2 * sizeof(s16));
}
} else {
// Remember that maximum volume allowed is 0xFFFFF so left shift is no issue.
// This way we can optimally shift by 16.
leftVol <<=1;
rightVol <<=1;
if (chan.format == PSP_AUDIO_FORMAT_STEREO) {
const u32 totalSamples = chan.sampleCount * 2;
s16_le *sampleData = (s16_le *) Memory::GetPointerOrException(chan.sampleAddress);
// Walking a pointer for speed. But let's make sure we wouldn't trip on an invalid ptr.
if (Memory::IsValidAddress(chan.sampleAddress + (totalSamples - 1) * sizeof(s16_le))) {
s16 *buf1 = 0, *buf2 = 0;
size_t sz1, sz2;
chanSampleQueues[chanNum].pushPointers(totalSamples, &buf1, &sz1, &buf2, &sz2);
AdjustVolumeBlock(buf1, sampleData, sz1, leftVol, rightVol);
if (buf2) {
AdjustVolumeBlock(buf2, sampleData + sz1, sz2, leftVol, rightVol);
}
}
} else if (chan.format == PSP_AUDIO_FORMAT_MONO) {
// Rare, so unoptimized. Expands to stereo.
if (Memory::IsValidRange(chan.sampleAddress, chan.sampleCount * sizeof(s16))) {
for (u32 i = 0; i < chan.sampleCount; i++) {
s16 sample = (s16)Memory::ReadUnchecked_U16(chan.sampleAddress + 2 * i);
chanSampleQueues[chanNum].push(ApplySampleVolume(sample, leftVol));
chanSampleQueues[chanNum].push(ApplySampleVolume(sample, rightVol));
}
}
}
}
return ret;
// Unity gain on the PSP is 0x8000. Accumulate at full width and clamp once at the end, the way
// the driver's 32-bit mix accumulator does, rather than clamping each channel separately.
// 64-bit because the SRC channel accepts volumes up to 0xFFFFF, which overflows a 32-bit
// product against a full-scale sample.
static inline int ApplyChannelVolume(int sample, int vol) {
return (int)(((s64)sample * vol) >> 15);
}
void __AudioWakeThreads(AudioChannel &chan, int result, int step) {
u32 error;
bool wokeThreads = false;
for (size_t w = 0; w < chan.waitingThreads.size(); ++w) {
AudioChannelWaitInfo &waitInfo = chan.waitingThreads[w];
waitInfo.numSamples -= step;
// Set while __AudioUpdate is running, so a buffer accepted from inside it - the retry a parked
// thread gets when its predecessor finishes - doesn't try to start the DMA again.
static bool audioMixing;
// Set while that mixing is happening underneath a syscall rather than from the timing event.
static bool audioMixingInSyscall;
// If it's done (there will still be samples on queue) and actually still waiting, wake it up.
u32 waitID = __KernelGetWaitID(waitInfo.threadID, WAITTYPE_AUDIOCHANNEL, error);
if (waitInfo.numSamples <= 0 && waitID != 0) {
// DEBUG_LOG(Log::sceAudio, "Woke thread %i for some buffer filling", waitingThread);
u32 ret = result == 0 ? __KernelGetWaitValue(waitInfo.threadID, error) : SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED;
__KernelResumeThreadFromWait(waitInfo.threadID, ret);
wokeThreads = true;
chan.waitingThreads.erase(chan.waitingThreads.begin() + w--);
}
// This means the thread stopped waiting, so stop trying to wake it.
else if (waitID == 0)
chan.waitingThreads.erase(chan.waitingThreads.begin() + w--);
}
if (wokeThreads) {
// Switching threads is fine from the timing event, but not from inside an output call: the
// syscall's return value is written after the call body runs, so a context switch here would
// put it in the wrong thread's registers. hleReSchedule defers to after the syscall instead.
static void __AudioReScheduleAfterWake() {
if (audioMixingInSyscall) {
hleReSchedule("audio drain");
} else {
__KernelReSchedule("audio drain");
}
}
void __AudioWakeThreads(AudioChannel &chan, int result) {
__AudioWakeThreads(chan, result, 0x7FFFFFFF);
// Only channels 0-7. The SRC channel is on its own DMA that the mixer never touches, so the
// two start independently of each other.
static bool __AudioAnyChannelPlaying() {
for (const AudioChannel &chan : g_audioChans) {
if (chan.sampleAddress != 0) {
return true;
}
}
return false;
}
// The driver starts the mixer's DMA the moment the first buffer arrives, and its mixer thread -
// which outranks whoever called - immediately fills a block from it. So the first 64 samples
// are gone before the output call has returned, and a channel reserved for exactly 64 samples
// is free again right away. Re-phasing the mix event to the buffer's arrival reproduces that
// and costs nothing: the interval, and so the sample rate, is unchanged.
static void __AudioStartMixerDMA() {
if (audioMixing) {
return;
}
// A mixer output is cheap - well under 10us - except for this one, which brings the DMA and
// the codec up and costs over 100us on hardware.
hleEatCycles(25000);
CoreTiming::UnscheduleEvent(eventAudioUpdate, 0);
audioMixingInSyscall = true;
__AudioUpdate();
audioMixingInSyscall = false;
CoreTiming::ScheduleEvent(audioIntervalCycles, eventAudioUpdate, 0);
}
// The SRC channel's DMA also starts when its first buffer arrives, but it feeds the codec
// directly rather than going through the mixer, so nothing is read early - only the phase moves.
// Without this the buffer would retire somewhere in the next 1.5ms depending on where the mix
// event happened to be, and a game polling sceAudioOutput2GetRestSample would see a different
// answer every run. Only safe to do while no mixer channel is playing, since the two share one
// event here and the mixer's phase is the one that has samples riding on it.
static void __AudioStartSRCDMA() {
if (audioMixing || __AudioAnyChannelPlaying()) {
return;
}
CoreTiming::UnscheduleEvent(eventAudioUpdate, 0);
CoreTiming::ScheduleEvent(audioIntervalCycles, eventAudioUpdate, 0);
}
u32 __AudioEnqueue(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol) {
if (!chan.reserved) {
return SCE_ERROR_AUDIO_CHANNEL_NOT_INIT;
}
// One buffer slot per channel, with no queue behind it.
if (chan.sampleAddress != 0) {
return SCE_ERROR_AUDIO_CHANNEL_BUSY;
}
chan.remainingSamples = chan.sampleCount;
if (leftVol >= 0) {
chan.leftVolume = leftVol;
}
if (rightVol >= 0) {
chan.rightVolume = rightVol;
}
// Handing over a buffer while nothing was playing is what starts the DMA.
const bool startsDMA = samplePtr != 0 && !__AudioAnyChannelPlaying();
// A null pointer is accepted and leaves the channel idle, but still counts as a buffer's
// worth of remaining samples - which is the one case where the two rest-length calls
// disagree with each other.
chan.sampleAddress = samplePtr;
if (startsDMA) {
__AudioStartMixerDMA();
}
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) {
return result;
}
// The driver keeps a single "a thread is waiting" flag per channel, so the second thread
// to arrive is turned away rather than lining up behind the first. A game that runs a
// movie thread and a sound-effect thread over one channel depends on being told this -
// blocking it instead makes the two take turns and halves the movie's audio rate.
if (chan.waitingThread != 0) {
return SCE_ERROR_AUDIO_CHANNEL_BUSY;
}
if (!__KernelIsDispatchEnabled()) {
return SCE_KERNEL_ERROR_CAN_NOT_WAIT;
}
chan.waitingThread = __KernelGetCurThread();
chan.waitingAddress = samplePtr;
chan.waitingLeftVolume = leftVol;
chan.waitingRightVolume = rightVol;
// __AudioChannelFinished retries the enqueue and supplies the real return value.
__KernelWaitCurThread(WAITTYPE_AUDIOCHANNEL, (SceUID)chan.index + 1, chan.sampleCount, 0, false, "blocking audio");
return chan.sampleCount;
}
// The buffer ran out - the driver's mixer would set this channel's bit in its event flag here.
static bool __AudioChannelFinished(AudioChannel &chan) {
chan.sampleAddress = 0;
chan.remainingSamples = 0;
if (chan.waitingThread == 0) {
return false;
}
const SceUID threadID = chan.waitingThread;
chan.waitingThread = 0;
u32 error;
if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) == 0) {
// It stopped waiting on its own - deleted, or the wait was cancelled.
return false;
}
__KernelResumeThreadFromWait(threadID, __AudioEnqueue(chan, chan.waitingAddress, chan.waitingLeftVolume, chan.waitingRightVolume));
return true;
}
// Measured on hardware with tests/audio/blocking/overhead. Every SRC output ends up querying
// the codec, which costs upwards of 100us, whether it armed a buffer, found the channel
// unreserved, or did nothing at all. The one shortcut is a channel with both descriptors
// already armed, which lands in the 30-100us range instead.
int __AudioSRCCallCycles(const AudioSRCChannel &chan) {
return chan.Full() ? 10000 : 25000;
}
u32 __AudioSRCEnqueueBlocking(AudioSRCChannel &chan, u32 samplePtr, int vol) {
if (!chan.reserved) {
return SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED;
}
// Two DMA descriptors, so two buffers fit. The third caller is refused outright - unlike
// the mixer channels it does not even get the chance to wait for a slot.
if (chan.Full()) {
return SCE_ERROR_AUDIO_CHANNEL_BUSY;
}
u32 result = 0;
if (samplePtr != 0) {
// The volume rides along with a buffer, so a null pointer leaves it alone. A negative
// one means the same thing.
if (vol >= 0) {
chan.leftVolume = vol;
chan.rightVolume = vol;
}
const bool wasIdle = chan.bufferCount == 0;
if (wasIdle) {
// Starting the DMA signals a completion by itself, which is why the first
// output after an idle stretch returns without blocking.
chan.completion = true;
chan.playedSamples = 0;
chan.frac = 0;
}
chan.buffers[chan.bufferCount].address = samplePtr;
chan.buffers[chan.bufferCount].samples = chan.sampleCount;
chan.bufferCount++;
result = chan.sampleCount;
if (wasIdle) {
__AudioStartSRCDMA();
}
} else if (chan.bufferCount == 0) {
// Nothing playing and nothing handed over, so there is no completion to wait for.
return 0;
}
if (chan.completion) {
chan.completion = false;
return result;
}
if (!__KernelIsDispatchEnabled()) {
return SCE_KERNEL_ERROR_CAN_NOT_WAIT;
}
chan.waitingThreads.push_back(__KernelGetCurThread());
__KernelWaitCurThread(WAITTYPE_AUDIOCHANNEL, PSP_AUDIO_SRC_WAIT_ID, result, 0, false, "blocking audio");
return result;
}
void __AudioSRCSignal(AudioSRCChannel &chan) {
chan.completion = true;
}
// One of the two SRC buffers finished playing.
static bool __AudioSRCCompleted(AudioSRCChannel &chan) {
// Threads that gave up on their own are dropped rather than counted as woken, so a
// completion is never spent on one - the next real waiter, or the flag, gets it.
while (!chan.waitingThreads.empty()) {
const SceUID threadID = chan.waitingThreads.front();
chan.waitingThreads.erase(chan.waitingThreads.begin());
u32 error;
if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) != 0) {
__KernelResumeThreadFromWait(threadID, __KernelGetWaitValue(threadID, error));
return true;
}
}
// Nobody is listening, so the completion sits there for the next caller to consume.
chan.completion = true;
return false;
}
void __AudioWakeThreads(AudioSRCChannel &chan, int result) {
bool woke = false;
for (SceUID threadID : chan.waitingThreads) {
u32 error;
if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) != 0) {
__KernelResumeThreadFromWait(threadID, result);
woke = true;
}
}
chan.waitingThreads.clear();
if (woke) {
// Only ever called from one of the release calls, so always inside a syscall.
hleReSchedule("audio drain");
}
}
void __AudioSetOutputFrequency(int freq) {
@@ -329,101 +481,144 @@ void __AudioSetSRCFrequency(int freq) {
srcFrequency = freq;
}
// Mixes one block from a mixer channel, reading straight out of the game's buffer.
static bool __AudioMixChannel(AudioChannel &chan) {
if (chan.sampleAddress == 0) {
// Idle, or holding the remaining count from a null-pointer output.
return false;
}
if (chan.remainingSamples == 0) {
// Can't happen from the API, since a channel is never reserved for zero samples, but a
// savestate could say otherwise - and left alone the channel would read as busy for
// ever, which is silence the game can't recover from.
return __AudioChannelFinished(chan);
}
const u32 count = std::min(chan.remainingSamples, (u32)hwBlockSize);
const bool mono = chan.format == PSP_AUDIO_FORMAT_MONO;
const u32 stride = mono ? 2 : 4;
// The samples are consumed either way; muting only drops them on the floor.
if (!chan.mute && Memory::IsValidRange(chan.sampleAddress, count * stride)) {
const s16_le *src = (const s16_le *)Memory::GetPointerUnchecked(chan.sampleAddress);
const int leftVol = chan.leftVolume;
const int rightVol = chan.rightVolume;
if (mono) {
// A mono channel reads the same sample into both sides, which is how the
// hardware expands it - there is no separate mono path in the mixer.
for (u32 s = 0; s < count; s++) {
const s16 sample = src[s];
mixBuffer[s * 2] += ApplyChannelVolume(sample, leftVol);
mixBuffer[s * 2 + 1] += ApplyChannelVolume(sample, rightVol);
}
} else {
for (u32 s = 0; s < count; s++) {
mixBuffer[s * 2] += ApplyChannelVolume(src[s * 2], leftVol);
mixBuffer[s * 2 + 1] += ApplyChannelVolume(src[s * 2 + 1], rightVol);
}
}
}
chan.sampleAddress += count * stride;
chan.remainingSamples -= count;
if (chan.remainingSamples == 0) {
return __AudioChannelFinished(chan);
}
return false;
}
// Channel 8 never reaches the mixer on hardware - the DMA feeds the codec directly and the
// codec resamples. Model that as a read straight through the pending buffers at the ratio
// between the reserved frequency and the output rate.
static bool __AudioMixSRC(AudioSRCChannel &chan) {
if (chan.bufferCount == 0) {
return false;
}
// 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;
const int rightVol = chan.rightVolume;
bool woke = false;
for (int out = 0; out < hwBlockSize; out++) {
if (chan.bufferCount == 0) {
// Underrun. The rest of the block stays silent, like a descriptor the game
// never got around to arming.
break;
}
const AudioPendingBuffer &buf = chan.buffers[0];
const u32 addr = buf.address + chan.playedSamples * 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];
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);
mixBuffer[out * 2] += ApplyChannelVolume(l0 + (((l1 - l0) * frac) >> 15), leftVol);
mixBuffer[out * 2 + 1] += ApplyChannelVolume(r0 + (((r1 - r0) * frac) >> 15), rightVol);
}
chan.frac += ratio;
u32 step = chan.frac >> 16;
chan.frac &= 0xFFFF;
while (step > 0 && chan.bufferCount > 0) {
const u32 take = std::min(step, chan.buffers[0].samples - chan.playedSamples);
chan.playedSamples += take;
step -= take;
if (chan.playedSamples >= chan.buffers[0].samples) {
chan.buffers[0] = chan.buffers[1];
chan.bufferCount--;
chan.playedSamples = 0;
woke |= __AudioSRCCompleted(chan);
}
}
}
return woke;
}
// Mix samples from the various audio channels into a single sample queue, managed by the backend implementation.
void __AudioUpdate(bool resetRecording) {
// AUDIO throttle doesn't really work on the PSP since the mixing intervals are so closely tied
// to the CPU. Much better to throttle the frame rate on frame display and just throw away audio
// if the buffer somehow gets full.
bool firstChannel = true;
const int16_t srcBufferSize = hwBlockSize * 2;
int16_t srcBuffer[srcBufferSize];
memset(mixBuffer, 0, hwBlockSize * 2 * sizeof(s32));
for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX + 1; i++) {
if (!g_audioChans[i].reserved) {
continue;
}
__AudioWakeThreads(g_audioChans[i], 0, hwBlockSize);
if (!chanSampleQueues[i].size()) {
continue;
}
bool needsResample = i == PSP_AUDIO_CHANNEL_SRC && srcFrequency != 0 && srcFrequency != mixFrequency;
size_t sz = needsResample ? (srcBufferSize * srcFrequency) / mixFrequency : srcBufferSize;
if (sz > chanSampleQueues[i].size()) {
ERROR_LOG(Log::sceAudio, "Channel %i buffer underrun at %i of %i", i, (int)chanSampleQueues[i].size() / 2, (int)sz / 2);
}
const s16 *buf1 = 0, *buf2 = 0;
size_t sz1, sz2;
chanSampleQueues[i].popPointers(sz, &buf1, &sz1, &buf2, &sz2);
// We do this check as the very last thing before mixing, to maximize compatibility.
if (g_audioChans[i].mute) {
continue;
}
if (needsResample) {
auto read = [&](size_t i) {
if (i < sz1)
return buf1[i];
if (i < sz1 + sz2)
return buf2[i - sz1];
if (buf2)
return buf2[sz2 - 1];
return buf1[sz1 - 1];
};
// TODO: This is terrible, since it's doing it by small chunk and discarding frac.
const uint32_t ratio = (uint32_t)(65536.0 * srcFrequency / (double)mixFrequency);
uint32_t frac = 0;
size_t readIndex = 0;
for (size_t outIndex = 0; readIndex < sz && outIndex < srcBufferSize; outIndex += 2) {
size_t readIndex2 = readIndex + 2;
int16_t l1 = read(readIndex);
int16_t r1 = read(readIndex + 1);
int16_t l2 = read(readIndex2);
int16_t r2 = read(readIndex2 + 1);
int sampleL = ((l1 << 16) + (l2 - l1) * (uint16_t)frac) >> 16;
int sampleR = ((r1 << 16) + (r2 - r1) * (uint16_t)frac) >> 16;
srcBuffer[outIndex] = sampleL;
srcBuffer[outIndex + 1] = sampleR;
frac += ratio;
readIndex += 2 * (uint16_t)(frac >> 16);
frac &= 0xffff;
}
buf1 = srcBuffer;
sz1 = srcBufferSize;
buf2 = nullptr;
sz2 = 0;
}
if (firstChannel) {
for (size_t s = 0; s < sz1; s++)
mixBuffer[s] = buf1[s];
if (buf2) {
for (size_t s = 0; s < sz2; s++)
mixBuffer[s + sz1] = buf2[s];
}
firstChannel = false;
} else {
// Surprisingly hard to SIMD efficiently on SSE2 due to lack of 16-to-32-bit sign extension. NEON should be straight-forward though, and SSE4.1 can do it nicely.
// Actually, the cmple/pack trick should work fine...
for (size_t s = 0; s < sz1; s++)
mixBuffer[s] += buf1[s];
if (buf2) {
for (size_t s = 0; s < sz2; s++)
mixBuffer[s + sz1] += buf2[s];
}
}
audioMixing = true;
bool woke = false;
for (AudioChannel &chan : g_audioChans) {
// Deliberately not gated on `reserved`: sceAudioChRelease only clears the
// reservation, and a buffer already in flight keeps playing out.
woke |= __AudioMixChannel(chan);
}
woke |= __AudioMixSRC(g_audioSRC);
audioMixing = false;
if (firstChannel) {
// Nothing was written above, let's memset.
memset(mixBuffer, 0, hwBlockSize * 2 * sizeof(s32));
if (woke) {
__AudioReScheduleAfterWake();
}
if (g_Config.bEnableSound) {
+24 -4
View File
@@ -41,10 +41,30 @@ void __AudioShutdown();
void __AudioSetOutputFrequency(int freq);
void __AudioSetSRCFrequency(int freq);
// May return SCE_ERROR_AUDIO_CHANNEL_BUSY if buffer too large
u32 __AudioEnqueue(AudioChannel &chan, int chanNum, bool blocking);
void __AudioWakeThreads(AudioChannel &chan, int result, int step);
void __AudioWakeThreads(AudioChannel &chan, int result);
// The driver's single enqueue point for channels 0-7. Returns the channel's reserved sample
// 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);
// Channel 8 - Output2, SRC and Vaudio all share it. Two buffers fit; a third caller gets
// SCE_ERROR_AUDIO_CHANNEL_BUSY without waiting. A successful call waits for one buffer to
// finish before returning, except when nothing was playing to begin with.
u32 __AudioSRCEnqueueBlocking(AudioSRCChannel &chan, u32 samplePtr, int vol);
// Hands the SRC channel a completion that the next caller can consume without waiting.
void __AudioSRCSignal(AudioSRCChannel &chan);
// What one SRC/Output2 output call costs, in cycles, before it does anything - see the comment
// on the definition.
int __AudioSRCCallCycles(const AudioSRCChannel &chan);
// Wake everyone parked on the SRC channel with the given error. Mixer channels need no
// equivalent: releasing one while a thread waits on it is refused outright.
void __AudioWakeThreads(AudioSRCChannel &chan, int result);
void __AudioCPUMHzChange();
+240 -140
View File
@@ -15,6 +15,8 @@
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <memory>
#include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Common/Data/Collections/FixedSizeQueue.h"
@@ -36,15 +38,18 @@ const int AUDIO_ROUTING_SPEAKER_ON = 1;
int defaultRoutingMode = AUDIO_ROUTING_SPEAKER_ON;
int defaultRoutingVolMode = AUDIO_ROUTING_SPEAKER_ON;
// TODO: These are way oversized and together consume 4MB of memory.
extern FixedSizeQueue<s16, 32768 * 8> chanSampleQueues[PSP_AUDIO_CHANNEL_MAX + 1];
// Only still here to read savestates written before the channels held buffer pointers.
struct AudioChannelWaitInfo {
SceUID threadID;
int numSamples;
};
// The extra channel is for SRC/Output2/Vaudio.
AudioChannel g_audioChans[PSP_AUDIO_CHANNEL_MAX + 1];
AudioChannel g_audioChans[PSP_AUDIO_CHANNEL_MAX];
// sceAudioOutput2, sceAudioSRC and sceVaudio are three names for this one channel.
AudioSRCChannel g_audioSRC;
void AudioChannel::DoState(PointerWrap &p)
{
auto s = p.Section("AudioChannel", 1, 3);
void AudioChannel::DoState(PointerWrap &p) {
auto s = p.Section("AudioChannel", 1, 4);
if (!s)
return;
@@ -54,81 +59,156 @@ void AudioChannel::DoState(PointerWrap &p)
Do(p, leftVolume);
Do(p, rightVolume);
Do(p, format);
Do(p, waitingThreads);
if (s >= 4) {
Do(p, remainingSamples);
Do(p, waitingThread);
Do(p, waitingAddress);
Do(p, waitingLeftVolume);
Do(p, waitingRightVolume);
return;
}
// Everything below is the old format, from when the emulator copied each buffer into a
// ring of samples at enqueue time instead of playing out of the game's memory. There is
// no way to turn that back into a buffer pointer and a position, so the pending audio is
// dropped - a fraction of a second of silence on load, and then the game carries on.
std::vector<AudioChannelWaitInfo> oldWaitingThreads;
Do(p, oldWaitingThreads);
if (s >= 2) {
// These are globals rather than per-channel; the old format just happened to write
// them out once per channel.
Do(p, defaultRoutingMode);
Do(p, defaultRoutingVolMode);
}
auto oldQueue = std::make_unique<FixedSizeQueue<s16, 32768 * 8>>();
if (s >= 3) {
// v3: compact queue form — only the live samples, not the whole 512KB
// fixed storage per channel. Cuts ~4.6MB of dead bytes from every
// savestate. Old savestates (s < 3) still load through the
// full-storage path below.
chanSampleQueues[index].DoStateCompact(p);
oldQueue->DoStateCompact(p);
} else {
chanSampleQueues[index].DoState(p);
oldQueue->DoState(p);
}
if (p.mode == p.MODE_READ) {
// Only the play position is dropped. reserved, sampleCount, the volumes and the format
// were all just read out of the state and are still good - clearing those as well
// leaves every channel unreserved, and the game's next output fails with
// SCE_ERROR_AUDIO_CHANNEL_NOT_INIT.
sampleAddress = 0;
remainingSamples = 0;
waitingThread = 0;
waitingAddress = 0;
waitingLeftVolume = 0;
waitingRightVolume = 0;
// The threads that were parked in a blocking output call are still parked, and
// nothing is going to wake them now, so hand them their buffer back.
for (const AudioChannelWaitInfo &waitInfo : oldWaitingThreads) {
u32 error;
if (__KernelGetWaitID(waitInfo.threadID, WAITTYPE_AUDIOCHANNEL, error) != 0) {
__KernelResumeThreadFromWait(waitInfo.threadID, sampleCount);
}
}
}
}
void AudioChannel::reset()
{
__AudioWakeThreads(*this, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED);
clear();
}
void AudioChannel::clear()
{
void AudioChannel::clear() {
reserved = false;
leftVolume = 0;
rightVolume = 0;
format = 0;
sampleAddress = 0;
sampleCount = 0;
chanSampleQueues[index].clear();
waitingThreads.clear();
sampleAddress = 0;
remainingSamples = 0;
waitingThread = 0;
waitingAddress = 0;
waitingLeftVolume = 0;
waitingRightVolume = 0;
}
// Enqueues the buffer pointed to on the channel. If channel buffer queue is full (2 items?) will block until it isn't.
// For solid audio output we'll need a queue length of 2 buffers at least.
void AudioSRCChannel::DoState(PointerWrap &p) {
auto s = p.Section("AudioSRCChannel", 1);
if (!s)
return;
Do(p, reserved);
Do(p, sampleCount);
Do(p, leftVolume);
Do(p, rightVolume);
Do(p, format);
Do(p, bufferCount);
if (bufferCount < 0 || bufferCount > (int)ARRAY_SIZE(buffers)) {
// Guarded because the enqueue indexes buffers[] with this, so a negative one would be
// a write outside the struct.
ERROR_LOG(Log::sceAudio, "Savestate failure: %d buffers on the SRC channel.", bufferCount);
p.SetError(p.ERROR_FAILURE);
return;
}
for (AudioPendingBuffer &buf : buffers) {
Do(p, buf.address);
Do(p, buf.samples);
}
Do(p, playedSamples);
Do(p, frac);
Do(p, completion);
Do(p, waitingThreads);
}
void __AudioRoutingDoState(PointerWrap &p) {
auto s = p.Section("sceAudioRouting", 1);
if (!s)
return;
Do(p, defaultRoutingMode);
Do(p, defaultRoutingVolMode);
}
void AudioSRCChannel::reset() {
__AudioWakeThreads(*this, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED);
clear();
}
void AudioSRCChannel::clear() {
reserved = false;
leftVolume = 0;
rightVolume = 0;
format = 0;
sampleCount = 0;
bufferCount = 0;
playedSamples = 0;
frac = 0;
completion = false;
waitingThreads.clear();
for (AudioPendingBuffer &buf : buffers) {
buf.address = 0;
buf.samples = 0;
}
}
// The blocking output calls do not queue callers up. Each channel holds one buffer and at most
// one parked thread; the SRC channel holds two buffers and no parked-thread slot at all. A
// caller that finds no room is told SCE_ERROR_AUDIO_CHANNEL_BUSY and expected to come back
// later. See docs/sceAudio.md.
// Not sure about the range of volume, I often see 0x800 so that might be either
// max or 50%?
static u32 sceAudioOutputBlocking(u32 chan, int vol, u32 samplePtr) {
if (vol > 0xFFFF) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume");
} else if (chan >= PSP_AUDIO_CHANNEL_MAX) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel");
} else if (!g_audioChans[chan].reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel not reserved");
}
if (vol >= 0) {
g_audioChans[chan].leftVolume = vol;
g_audioChans[chan].rightVolume = vol;
}
g_audioChans[chan].sampleAddress = samplePtr;
return hleLogDebug(Log::sceAudio, __AudioEnqueue(g_audioChans[chan], chan, true));
return hleLogDebug(Log::sceAudio, __AudioEnqueueBlocking(g_audioChans[chan], samplePtr, vol, vol));
}
static u32 sceAudioOutputPannedBlocking(u32 chan, int leftvol, int rightvol, u32 samplePtr) {
// For some reason, this is the only one that checks for negative.
if (leftvol > 0xFFFF || rightvol > 0xFFFF || leftvol < 0 || rightvol < 0) {
// This one ORs the two volumes together before comparing, so unlike the others a negative
// volume fails instead of meaning "leave it alone".
if ((u32)(leftvol | rightvol) > 0xFFFF) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume");
} else if (chan >= PSP_AUDIO_CHANNEL_MAX) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel");
} else if (!g_audioChans[chan].reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel not reserved");
}
if (leftvol >= 0) {
g_audioChans[chan].leftVolume = leftvol;
}
if (rightvol >= 0) {
g_audioChans[chan].rightVolume = rightvol;
}
g_audioChans[chan].sampleAddress = samplePtr;
u32 result = __AudioEnqueue(g_audioChans[chan], chan, true);
return hleLogDebug(Log::sceAudio, result);
return hleLogDebug(Log::sceAudio, __AudioEnqueueBlocking(g_audioChans[chan], samplePtr, leftvol, rightvol));
}
static u32 sceAudioOutput(u32 chan, int vol, u32 samplePtr) {
@@ -136,17 +216,9 @@ static u32 sceAudioOutput(u32 chan, int vol, u32 samplePtr) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume");
} else if (chan >= PSP_AUDIO_CHANNEL_MAX) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel");
} else if (!g_audioChans[chan].reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel not reserved");
}
if (vol >= 0) {
g_audioChans[chan].leftVolume = vol;
g_audioChans[chan].rightVolume = vol;
}
g_audioChans[chan].sampleAddress = samplePtr;
u32 result = __AudioEnqueue(g_audioChans[chan], chan, false);
return hleLogDebug(Log::sceAudio, result);
return hleLogDebug(Log::sceAudio, __AudioEnqueue(g_audioChans[chan], samplePtr, vol, vol));
}
static u32 sceAudioOutputPanned(u32 chan, int leftvol, int rightvol, u32 samplePtr) {
@@ -154,43 +226,44 @@ static u32 sceAudioOutputPanned(u32 chan, int leftvol, int rightvol, u32 sampleP
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume");
} else if (chan >= PSP_AUDIO_CHANNEL_MAX) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel");
} else if (!g_audioChans[chan].reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel not reserved");
} else {
if (leftvol >= 0) {
g_audioChans[chan].leftVolume = leftvol;
}
if (rightvol >= 0) {
g_audioChans[chan].rightVolume = rightvol;
}
g_audioChans[chan].sampleAddress = samplePtr;
u32 result = __AudioEnqueue(g_audioChans[chan], chan, false);
return hleLogDebug(Log::sceAudio, result);
}
return hleLogDebug(Log::sceAudio, __AudioEnqueue(g_audioChans[chan], samplePtr, leftvol, rightvol));
}
// A thread parked in a blocking output call counts as a whole extra buffer, on top of whatever
// is left of the one playing.
static int sceAudioGetChannelRestLen(u32 chan) {
if (chan >= PSP_AUDIO_CHANNEL_MAX) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel");
}
int remainingSamples = (int)chanSampleQueues[chan].size() / 2;
return hleLogVerbose(Log::sceAudio, remainingSamples);
const AudioChannel &c = g_audioChans[chan];
int rest = (int)c.remainingSamples;
if (c.waitingThread != 0) {
rest += (int)c.sampleCount;
}
return hleLogVerbose(Log::sceAudio, rest);
}
static int sceAudioGetChannelRestLength(u32 chan) {
if (chan >= PSP_AUDIO_CHANNEL_MAX) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel");
}
int remainingSamples = (int)chanSampleQueues[chan].size() / 2;
return hleLogVerbose(Log::sceAudio, remainingSamples);
// Unlike its sibling this one checks that a buffer is really playing first, so after an
// output with a null pointer the two disagree.
const AudioChannel &c = g_audioChans[chan];
int rest = c.sampleAddress != 0 ? (int)c.remainingSamples : 0;
if (c.waitingThread != 0) {
rest += (int)c.sampleCount;
}
return hleLogVerbose(Log::sceAudio, rest);
}
static int GetFreeChannel() {
// Changed this to allow channel 0. Fixes the startup sound in VSH. TODO: Why did we not allow channel 0 before?
// The counter has to be signed: as a u32 the i >= 0 condition is always true, so with every
// channel reserved it wrapped past zero and ran off the array instead of giving up.
// The search runs downwards from 7, and a channel only counts as free once it has both
// been released and finished playing whatever it still held.
for (int i = PSP_AUDIO_CHANNEL_MAX - 1; i >= 0; --i) {
if (!g_audioChans[i].reserved)
if (g_audioChans[i].sampleCount == 0 && g_audioChans[i].sampleAddress == 0)
return i;
}
return -1;
@@ -206,15 +279,15 @@ static u32 sceAudioChReserve(int chan, u32 sampleCount, u32 format) {
if ((u32)chan >= PSP_AUDIO_CHANNEL_MAX) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel %d", chan);
}
if (g_audioChans[chan].reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "reserve channel failed");
}
if ((sampleCount & 63) != 0 || sampleCount == 0 || sampleCount > PSP_AUDIO_SAMPLE_MAX) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_OUTPUT_SAMPLE_DATA_SIZE_NOT_ALIGNED, "invalid sample count (not aligned)");
}
if (format != PSP_AUDIO_FORMAT_MONO && format != PSP_AUDIO_FORMAT_STEREO) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_FORMAT, "invalid format");
}
if (g_audioChans[chan].reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "reserve channel failed");
}
g_audioChans[chan].sampleCount = sampleCount;
g_audioChans[chan].format = format;
@@ -228,24 +301,30 @@ static u32 sceAudioChRelease(u32 chan) {
if (chan >= PSP_AUDIO_CHANNEL_MAX) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel %d", chan);
} else if (!g_audioChans[chan].reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel %d not reserved", chan);
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel %d not reserved", chan);
} else if (g_audioChans[chan].waitingThread != 0) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_BUSY, "channel %d has a thread waiting", chan);
}
// TODO: Does this error if busy?
g_audioChans[chan].reset();
// Only the reservation goes away. A buffer already handed over keeps playing to the end,
// and the channel stays unavailable to sceAudioChReserve(-1) until it does.
g_audioChans[chan].reserved = false;
g_audioChans[chan].sampleCount = 0;
return hleLogDebug(Log::sceAudio, 0);
}
static u32 sceAudioSetChannelDataLen(u32 chan, u32 len) {
if (chan >= PSP_AUDIO_CHANNEL_MAX) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel %d", chan);
} else if (!g_audioChans[chan].reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel %d not reserved", chan);
} else if ((len & 63) != 0 || len == 0 || len > PSP_AUDIO_SAMPLE_MAX) {
// Checked before the reservation, unlike most of the others.
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_OUTPUT_SAMPLE_DATA_SIZE_NOT_ALIGNED, "invalid sample count");
} else if (g_audioChans[chan].waitingThread != 0) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_BUSY, "channel %d has a thread waiting", chan);
} else if (!g_audioChans[chan].reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel %d not reserved", chan);
}
g_audioChans[chan].sampleCount = len;
return hleLogDebug(Log::sceAudio, 0);
}
@@ -253,25 +332,60 @@ static u32 sceAudioSetChannelDataLen(u32 chan, u32 len) {
static u32 sceAudioChangeChannelConfig(u32 chan, u32 format) {
if (chan >= PSP_AUDIO_CHANNEL_MAX) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "invalid channel number %d", chan);
} else if (g_audioChans[chan].waitingThread != 0 || g_audioChans[chan].sampleAddress != 0) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_BUSY, "channel %d busy", chan);
} else if (!g_audioChans[chan].reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel %d not reserved", chan);
} else if (format != PSP_AUDIO_FORMAT_MONO && format != PSP_AUDIO_FORMAT_STEREO) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_FORMAT, "invalid format");
}
g_audioChans[chan].format = format;
return hleLogDebug(Log::sceAudio, 0);
}
static u32 sceAudioChangeChannelVolume(u32 chan, u32 leftvol, u32 rightvol) {
// 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);
} else if (chan >= PSP_AUDIO_CHANNEL_MAX) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "invalid channel %d", chan);
} else if (!g_audioChans[chan].reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel %d not reserved", chan);
}
g_audioChans[chan].leftVolume = leftvol;
g_audioChans[chan].rightVolume = rightvol;
// There is no reservation check here, and a negative volume means "leave that side alone".
if (leftvol >= 0) {
g_audioChans[chan].leftVolume = leftvol;
}
if (rightvol >= 0) {
g_audioChans[chan].rightVolume = rightvol;
}
return hleLogDebug(Log::sceAudio, 0);
}
@@ -286,7 +400,7 @@ static u32 sceAudioEnd() {
}
static u32 sceAudioOutput2Reserve(u32 sampleCount) {
auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2];
auto &chan = g_audioSRC;
// This seems to ignore the MSB, for some reason.
sampleCount &= 0x7FFFFFFF;
if (sampleCount < 17 || sampleCount > 4111) {
@@ -295,6 +409,7 @@ static u32 sceAudioOutput2Reserve(u32 sampleCount) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED, "channel already reserved");
}
chan.clear();
chan.sampleCount = sampleCount;
chan.format = PSP_AUDIO_FORMAT_STEREO;
chan.reserved = true;
@@ -308,53 +423,47 @@ static u32 sceAudioOutput2OutputBlocking(u32 vol, u32 dataPtr) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume");
}
auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2];
if (!chan.reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved");
}
chan.leftVolume = vol;
chan.rightVolume = vol;
chan.sampleAddress = dataPtr;
hleEatCycles(10000);
int result = __AudioEnqueue(chan, PSP_AUDIO_CHANNEL_OUTPUT2, true);
if (result < 0)
return hleLogError(Log::sceAudio, result);
return hleLogDebug(Log::sceAudio, result);
hleEatCycles(__AudioSRCCallCycles(g_audioSRC));
// A busy channel is an ordinary answer here that a game is expected to poll on, not a
// fault, so this stays at debug like the mixer channels rather than filling the error log.
return hleLogDebug(Log::sceAudio, __AudioSRCEnqueueBlocking(g_audioSRC, dataPtr, vol));
}
static u32 sceAudioOutput2ChangeLength(u32 sampleCount) {
auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2];
// The length is range-checked before the channel is, and 4111 is the same ceiling the
// reserve takes.
if (sampleCount - 17 >= 0xFFF) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_OUTPUT_SAMPLE_DATA_SIZE_NOT_ALIGNED, "invalid sample count");
}
auto &chan = g_audioSRC;
if (!chan.reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved");
}
// Buffers already handed over keep their original length; only what is reported and what
// is accepted from here on changes.
chan.sampleCount = sampleCount;
return hleLogDebug(Log::sceAudio, 0);
}
static u32 sceAudioOutput2GetRestSample() {
auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2];
auto &chan = g_audioSRC;
if (!chan.reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved");
}
u32 size = (u32)chanSampleQueues[PSP_AUDIO_CHANNEL_OUTPUT2].size() / 2;
if (size > chan.sampleCount) {
// If ChangeLength reduces the size, it still gets output but this return is clamped.
size = chan.sampleCount;
}
return hleLogDebug(Log::sceAudio, size);
// Counts armed DMA descriptors, in units of the current length - so it reports two
// buffers' worth while both are in flight.
return hleLogDebug(Log::sceAudio, chan.bufferCount * chan.sampleCount);
}
static u32 sceAudioOutput2Release() {
auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2];
auto &chan = g_audioSRC;
if (!chan.reserved)
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved");
if (!chanSampleQueues[PSP_AUDIO_CHANNEL_OUTPUT2].empty())
if (chan.bufferCount != 0)
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED, "output busy");
chan.reset();
chan.reserved = false;
__AudioSRCSignal(chan);
return hleLogDebug(Log::sceAudio, 0);
}
@@ -384,7 +493,7 @@ bool SRCFrequencyAllowed(int freq) {
}
static u32 sceAudioSRCChReserve(u32 sampleCount, u32 freq, u32 format) {
auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_SRC];
auto &chan = g_audioSRC;
// This seems to ignore the MSB, for some reason.
sampleCount &= 0x7FFFFFFF;
if (format == 4) {
@@ -399,6 +508,7 @@ static u32 sceAudioSRCChReserve(u32 sampleCount, u32 freq, u32 format) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED, "channel already reserved");
}
chan.clear();
chan.reserved = true;
chan.sampleCount = sampleCount;
chan.format = format == 2 ? PSP_AUDIO_FORMAT_STEREO : PSP_AUDIO_FORMAT_MONO;
@@ -408,14 +518,15 @@ static u32 sceAudioSRCChReserve(u32 sampleCount, u32 freq, u32 format) {
}
static u32 sceAudioSRCChRelease() {
auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_SRC];
auto &chan = g_audioSRC;
if (!chan.reserved)
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved");
if (!chanSampleQueues[PSP_AUDIO_CHANNEL_SRC].empty())
if (chan.bufferCount != 0)
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED, "output busy");
chan.reset();
chan.reserved = false;
// Releasing signals a completion, which the next caller after a fresh reserve consumes.
__AudioSRCSignal(chan);
return hleLogDebug(Log::sceAudio, 0);
}
@@ -424,20 +535,8 @@ static u32 sceAudioSRCOutputBlocking(u32 vol, u32 buf) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume");
}
auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_SRC];
if (!chan.reserved) {
return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved");
}
chan.leftVolume = vol;
chan.rightVolume = vol;
chan.sampleAddress = buf;
hleEatCycles(10000);
int result = __AudioEnqueue(chan, PSP_AUDIO_CHANNEL_SRC, true);
if (result < 0)
return hleLogError(Log::sceAudio, result);
return hleLogDebug(Log::sceAudio, result);
hleEatCycles(__AudioSRCCallCycles(g_audioSRC));
return hleLogDebug(Log::sceAudio, __AudioSRCEnqueueBlocking(g_audioSRC, buf, vol));
}
static int sceAudioInputBlocking(u32 maxSamples, u32 sampleRate, u32 bufAddr) {
@@ -531,15 +630,16 @@ const HLEFunction sceAudio[] =
{0XB011922F, &WrapI_U<sceAudioGetChannelRestLength>, "sceAudioGetChannelRestLength", 'i', "i" },
{0XCB2E439E, &WrapU_UU<sceAudioSetChannelDataLen>, "sceAudioSetChannelDataLen", 'x', "ii" },
{0X95FD0C2D, &WrapU_UU<sceAudioChangeChannelConfig>, "sceAudioChangeChannelConfig", 'x', "ii" },
{0XB7E1D8E7, &WrapU_UUU<sceAudioChangeChannelVolume>, "sceAudioChangeChannelVolume", 'x', "ixx" },
{0XB7E1D8E7, &WrapU_UII<sceAudioChangeChannelVolume>, "sceAudioChangeChannelVolume", 'x', "ixx" },
// Like Output2, but with ability to do sample rate conversion.
{0X38553111, &WrapU_UUU<sceAudioSRCChReserve>, "sceAudioSRCChReserve", 'x', "iii" },
{0X5C37C0AE, &WrapU_V<sceAudioSRCChRelease>, "sceAudioSRCChRelease", 'x', "" },
{0XE0727056, &WrapU_UU<sceAudioSRCOutputBlocking>, "sceAudioSRCOutputBlocking", 'x', "xx" },
// Never seen these used
{0X41EFADE7, nullptr, "sceAudioOneshotOutput", '?', "" },
{0X41EFADE7, &WrapU_IIIIIU<sceAudioOneshotOutput>, "sceAudioOneshotOutput", 'x', "iiiiix"},
// Never seen this used
{0XB61595C0, nullptr, "sceAudioLoopbackTest", '?', "" },
// Microphone interface
+63 -13
View File
@@ -17,7 +17,7 @@
#pragma once
#include <queue>
#include <vector>
#include "CommonTypes.h"
#include "sceKernel.h"
@@ -27,24 +27,26 @@ class PointerWrap;
enum PspAudioFormats { PSP_AUDIO_FORMAT_STEREO = 0, PSP_AUDIO_FORMAT_MONO = 0x10 };
enum PspAudioFrequencies { PSP_AUDIO_FREQ_44K = 44100, PSP_AUDIO_FREQ_48K = 48000 };
// Channels 0-7, the ones the software mixer walks.
const u32 PSP_AUDIO_CHANNEL_MAX = 8;
const int PSP_AUDIO_CHANNEL_SRC = 8;
const int PSP_AUDIO_CHANNEL_OUTPUT2 = 8;
const int PSP_AUDIO_CHANNEL_VAUDIO = 8;
// Mixer channels wait on their own index plus one, so the SRC channel takes the id after them.
const int PSP_AUDIO_SRC_WAIT_ID = PSP_AUDIO_CHANNEL_MAX + 1;
struct AudioChannelWaitInfo {
SceUID threadID;
int numSamples;
// One buffer handed over and not yet fully played.
struct AudioPendingBuffer {
u32 address;
u32 samples;
};
// A channel the software mixer walks. It holds exactly one buffer, played straight out of the
// game's memory 64 samples at a time, and has room for exactly one parked thread. See
// docs/sceAudio.md.
struct AudioChannel {
int index = 0;
bool reserved = false;
// last sample address
u32 sampleAddress = 0;
u32 sampleCount = 0; // Number of samples written in each OutputBlocking
u32 sampleCount = 0; // Buffer size agreed at reserve time.
u32 leftVolume = 0;
u32 rightVolume = 0;
u32 format = 0;
@@ -52,19 +54,67 @@ struct AudioChannel {
// For the debugger only. Not saved.
bool mute = false;
std::vector<AudioChannelWaitInfo> waitingThreads;
// sampleAddress walks forward as the mixer consumes the buffer and drops back to zero when
// remainingSamples runs out. An output with a null pointer sets remainingSamples but leaves
// sampleAddress at zero, which is the one case where the two rest-length calls disagree.
u32 sampleAddress = 0;
u32 remainingSamples = 0;
// A second thread arriving while one is parked here is told the channel is busy rather than
// queueing up behind it. These remember what the parked one wanted to hand over, so the
// enqueue can be retried once the buffer finishes.
SceUID waitingThread = 0;
u32 waitingAddress = 0;
int waitingLeftVolume = 0;
int waitingRightVolume = 0;
void DoState(PointerWrap &p);
void clear();
};
// Channel 8, and there is only one of it: sceAudioOutput2, sceAudioSRC and sceVaudio are three
// names for the same channel. It skips the mixer entirely - its two DMA descriptors point
// straight at the game's buffers and the codec resamples them - so it shares nothing with the
// mixer channels beyond what reserve agrees on. Two buffers fit; a third caller is turned away.
struct AudioSRCChannel {
bool reserved = false;
u32 sampleCount = 0; // Buffer size agreed at reserve time.
u32 leftVolume = 0;
u32 rightVolume = 0;
u32 format = 0;
// For the debugger only. Not saved.
bool mute = false;
AudioPendingBuffer buffers[2]{};
int bufferCount = 0;
u32 playedSamples = 0; // Consumed from buffers[0].
u32 frac = 0; // 16.16 position between two input samples, for resampling.
// The driver signals a finished buffer with an event flag bit, so one completion can sit
// there unclaimed - which is why the first output after an idle period doesn't block.
bool completion = false;
std::vector<SceUID> waitingThreads;
void DoState(PointerWrap &p);
void reset();
void clear();
bool Full() const {
return bufferCount >= (int)ARRAY_SIZE(buffers);
}
};
// The extra channel is for SRC/Output2/Vaudio (who all share, apparently.)
extern AudioChannel g_audioChans[PSP_AUDIO_CHANNEL_MAX + 1];
extern AudioChannel g_audioChans[PSP_AUDIO_CHANNEL_MAX];
extern AudioSRCChannel g_audioSRC;
// The sample rates the SRC and VAUDIO channels will accept.
bool SRCFrequencyAllowed(int freq);
// The two routing modes are globals rather than per-channel; they get their own little block.
void __AudioRoutingDoState(PointerWrap &p);
void Register_sceAudio();
+40 -22
View File
@@ -55,49 +55,67 @@ static u32 sceVaudioChReserve(int sampleCount, int freq, int format) {
ERROR_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i) - unexpected format", sampleCount, freq, format);
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
if (freq != 0 && !SRCFrequencyAllowed(freq)) {
ERROR_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i) - invalid frequency", sampleCount, freq, format);
return SCE_ERROR_AUDIO_INVALID_FREQUENCY;
}
if (vaudioReserved) {
ERROR_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i) - already reserved", sampleCount, freq, format);
return SCE_KERNEL_ERROR_BUSY;
}
// Everything past here is the underlying sceAudioSRCChReserve, and the module marks itself
// reserved before handing over - it does not undo that when the reserve fails. So a caller
// that got 0x80268002 because Output2 held the channel is told 0x80000021 next time round,
// until a release clears it.
vaudioReserved = true;
if (freq != 0 && !SRCFrequencyAllowed(freq)) {
ERROR_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i) - invalid frequency", sampleCount, freq, format);
return SCE_ERROR_AUDIO_INVALID_FREQUENCY;
}
// We still have to check the channel also, which gives a different error.
if (g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].reserved) {
if (g_audioSRC.reserved) {
ERROR_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i) - channel already reserved", sampleCount, freq, format);
return SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED;
}
DEBUG_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i)", sampleCount, freq, format);
g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].reserved = true;
g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].sampleCount = sampleCount;
g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].format = format == 2 ? PSP_AUDIO_FORMAT_STEREO : PSP_AUDIO_FORMAT_MONO;
g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].leftVolume = 0;
g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].rightVolume = 0;
vaudioReserved = true;
g_audioSRC.clear();
g_audioSRC.reserved = true;
g_audioSRC.sampleCount = sampleCount;
g_audioSRC.format = format == 2 ? PSP_AUDIO_FORMAT_STEREO : PSP_AUDIO_FORMAT_MONO;
__AudioSetSRCFrequency(freq);
return 0;
}
static u32 sceVaudioChRelease() {
DEBUG_LOG(Log::sceAudio, "sceVaudioChRelease(...)");
if (!g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].reserved) {
// Not gated on vaudio's own flag: the release goes straight at the SRC channel, so it will
// happily release a reservation that Output2 made. pspautotests calls that the "wrong
// release" and the hardware allows it.
vaudioReserved = false;
if (!g_audioSRC.reserved) {
return SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED;
} else {
g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].reset();
g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].reserved = false;
vaudioReserved = false;
return 0;
}
// Unlike the Output2 and SRC releases, which refuse while a buffer is in flight, this one
// hands the channel a null pointer first. That parks the caller until a buffer finishes, so
// what was playing is played out instead of being cut off.
__AudioSRCEnqueueBlocking(g_audioSRC, 0, -1);
if (g_audioSRC.Full()) {
// One drain was not enough to free a descriptor, so the release itself fails.
return SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED;
}
// The reservation goes now rather than when the drain finishes. The caller is parked either
// way and gets the same answer at the same time; the buffers keep playing because the mixer
// does not look at the reservation. Only another thread peeking during that window could
// tell the difference.
g_audioSRC.reserved = false;
g_audioSRC.sampleCount = 0;
__AudioSRCSignal(g_audioSRC);
return 0;
}
static u32 sceVaudioOutputBlocking(int vol, u32 buffer) {
DEBUG_LOG(Log::sceAudio, "sceVaudioOutputBlocking(%i, %08x)", vol, buffer);
g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].leftVolume = vol;
g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].rightVolume = vol;
// TODO: This may be wrong, not sure if's in a different format?
g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].sampleAddress = buffer;
return __AudioEnqueue(g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO], PSP_AUDIO_CHANNEL_VAUDIO, true);
// Shares the SRC channel, so it also shares the two-buffer depth and the busy return.
return __AudioSRCEnqueueBlocking(g_audioSRC, buffer, vol);
}
static u32 sceVaudioSetEffectType(int effectType, int vol) {
+55 -25
View File
@@ -1630,46 +1630,76 @@ void DrawAudioChannels(ImConfig &cfg, ImControl &control) {
ImGui::TableHeadersRow();
// vaudio / output2 uses channel 8.
for (int i = 0; i < PSP_AUDIO_CHANNEL_MAX + 1; i++) {
if (!g_audioChans[i].reserved) {
static const auto formatName = [](u32 format) {
switch (format) {
case PSP_AUDIO_FORMAT_STEREO: return "Stereo";
case PSP_AUDIO_FORMAT_MONO: return "Mono";
default: return "UNK";
}
};
static const auto threadName = [](SceUID threadID) -> const char * {
KernelObject *thread = kernelObjects.GetFast<KernelObject>(threadID);
return thread ? thread->GetName() : nullptr;
};
for (int i = 0; i < (int)PSP_AUDIO_CHANNEL_MAX; i++) {
const AudioChannel &chan = g_audioChans[i];
if (!chan.reserved) {
continue;
}
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::PushID(i);
if (i == 8) {
ImGui::TextUnformatted("audio2");
} else {
ImGui::Text("%d", i);
}
ImGui::Text("%d", i);
ImGui::TableNextColumn();
ImGui::Checkbox("", &g_audioChans[i].mute);
ImGui::TableNextColumn();
char id[2]{};
id[0] = i + 1;
ImClickableValue(id, g_audioChans[i].sampleAddress, control, ImCmd::SHOW_IN_MEMORY_VIEWER);
ImClickableValue(id, chan.sampleAddress, control, ImCmd::SHOW_IN_MEMORY_VIEWER);
ImGui::TableNextColumn();
ImGui::Text("%08x", g_audioChans[i].sampleCount);
ImGui::Text("%08x", chan.sampleCount);
ImGui::TableNextColumn();
ImGui::Text("%d | %d", g_audioChans[i].leftVolume, g_audioChans[i].rightVolume);
ImGui::Text("%d | %d", chan.leftVolume, chan.rightVolume);
ImGui::TableNextColumn();
switch (g_audioChans[i].format) {
case PSP_AUDIO_FORMAT_STEREO:
ImGui::TextUnformatted("Stereo");
break;
case PSP_AUDIO_FORMAT_MONO:
ImGui::TextUnformatted("Mono");
break;
default:
ImGui::TextUnformatted("UNK: %04x");
break;
ImGui::TextUnformatted(formatName(chan.format));
ImGui::TableNextColumn();
// Only one thread can ever be parked on a mixer channel.
if (chan.waitingThread != 0) {
const char *name = threadName(chan.waitingThread);
if (name) {
ImGui::TextUnformatted(name);
}
}
ImGui::PopID();
}
// Output2, SRC and Vaudio all mean this one, which holds two buffers rather than one
// and can have more than one thread waiting on it.
if (g_audioSRC.reserved) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::PushID("src");
ImGui::TextUnformatted("audio2");
ImGui::TableNextColumn();
ImGui::Checkbox("", &g_audioSRC.mute);
ImGui::TableNextColumn();
if (g_audioSRC.bufferCount > 0) {
ImClickableValue("src", g_audioSRC.buffers[0].address, control, ImCmd::SHOW_IN_MEMORY_VIEWER);
} else {
ImGui::TextUnformatted("-");
}
ImGui::TableNextColumn();
for (auto t : g_audioChans[i].waitingThreads) {
KernelObject *thread = kernelObjects.GetFast<KernelObject>(t.threadID);
if (thread) {
ImGui::Text("%s: %d", thread->GetName(), t.numSamples);
ImGui::Text("%08x (%d queued)", g_audioSRC.sampleCount, g_audioSRC.bufferCount);
ImGui::TableNextColumn();
ImGui::Text("%d | %d", g_audioSRC.leftVolume, g_audioSRC.rightVolume);
ImGui::TableNextColumn();
ImGui::TextUnformatted(formatName(g_audioSRC.format));
ImGui::TableNextColumn();
for (SceUID threadID : g_audioSRC.waitingThreads) {
const char *name = threadName(threadID);
if (name) {
ImGui::TextUnformatted(name);
}
}
ImGui::PopID();
+200
View File
@@ -0,0 +1,200 @@
# sceAudio: how the PSP's audio output actually behaves
This describes the behavior `Core/HLE/sceAudio.cpp` and `Core/HLE/__sceAudio.cpp` model, and
where that behavior came from. Confirmed on a real PSP by `pspautotests/tests/audio/blocking`.
The headline, because it is the thing most likely to be assumed wrong: **the blocking output
calls are not a queue that callers line up behind.** Each channel holds one buffer, the
Output2/SRC channel holds two, and a caller who finds no room is told
`SCE_ERROR_AUDIO_CHANNEL_BUSY` and expected to go away and come back.
## Two different pieces of hardware
`sceAudio` presents nine channels, but they are not the same thing underneath, which is why the
emulator keeps them in two different structures rather than one array of nine: `AudioChannel`
for the eight the mixer walks, and a single `AudioSRCChannel` for the ninth.
**Channels 0-7** go through a software mixer running as a kernel thread. Every time the audio
DMA finishes a block, that thread wakes, takes up to 64 samples from each channel that has a
buffer, sums them into a 32-bit accumulator, clamps, and writes 64 stereo frames into the other
half of a double buffer.
**Channel 8** - `sceAudioOutput2*`, `sceAudioSRC*` and `sceVaudio*` are all the same channel -
never touches the mixer. It builds DMA descriptors that point straight at the game's buffer and
lets the codec resample it. `sceAudioOutput2Reserve(n)` is literally
`sceAudioSRCChReserve(n, 44100, 2)`, and the other Output2 entry points are one-instruction tail
calls to the SRC ones.
They also have separate DMA channels, so one starting does not disturb the other.
## The driver plays out of the game's memory
Nothing is copied when a buffer is handed over. `sceAudioOutputBlocking` stores the pointer, and
the mixer walks it forward 64 samples at a time until it is spent, then clears it and signals.
`sceAudioGetChannelRestLen` counts down in steps of 64 for exactly that reason.
The emulator used to copy the whole buffer into a ring of samples at enqueue time, with the
volume already applied. That made a game rewriting a buffer it had already handed over
invisible, and it made the queue depth an emulator choice rather than a hardware fact.
## One buffer per channel
The driver's single enqueue point, which all four `sceAudioOutput*` functions reach, is:
```c
if (sampleCount == 0) return SCE_ERROR_AUDIO_CHANNEL_NOT_INIT;
if (sampleAddress != 0) return SCE_ERROR_AUDIO_CHANNEL_BUSY;
remaining = sampleCount;
sampleAddress = ptr; // may be 0
return sampleCount;
```
`sceAudioOutput` and `sceAudioOutputPanned` return that result as it stands, so a second
non-blocking output is refused until the first buffer has finished.
The blocking pair adds a wait, but only for the first thread to ask:
```c
r = enqueue(...);
if (r != BUSY) return r;
if (channel->waiting) return BUSY; // somebody else is already parked here
channel->waiting = 1;
wait for this channel's bit in the driver's event flag;
retry the enqueue;
channel->waiting = 0;
```
**Only one thread can be parked on a channel.** That single flag is why a game that runs a movie
thread and a sound-effect thread over one output gets sensible behavior on hardware and did not
in the emulator: the loser is told the channel is busy and skips its turn, where an emulator that
blocked it instead made the two threads alternate, halving the movie's audio rate. That is
https://github.com/hrydgard/ppsspp/issues/12888, and it is what
`tests/audio/blocking/contend` pins down.
The same flag locks out `sceAudioChRelease`, `sceAudioSetChannelDataLen` and
`sceAudioChangeChannelConfig`, which all return `SCE_ERROR_AUDIO_CHANNEL_BUSY` while a thread is
parked. `sceAudioChangeChannelVolume` does not check anything.
A null pointer is accepted and sets `remaining` without setting `sampleAddress`, so nothing
plays. That is the documented way to wait for a channel to drain, and it is the one case where
the two rest-length calls disagree - see below.
## Two buffers on the SRC channel
Channel 8 has two DMA descriptors, so two buffers can be in flight. The third caller is refused
outright and does not even get the chance to wait:
```c
if (!reserved) return SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED;
if (both descriptors armed) return SCE_ERROR_AUDIO_CHANNEL_BUSY;
arm one;
wait for one descriptor to retire;
return sampleCount;
```
Starting the DMA sets the same event flag bit that a retiring descriptor does, which is why the
first output after an idle stretch returns without waiting while every one after it waits exactly
one buffer. The emulator models that unclaimed completion with a flag on the channel.
## The two rest-length calls are not the same function twice
```c
sceAudioGetChannelRestLen(ch) = remaining + (waiting ? sampleCount : 0)
sceAudioGetChannelRestLength(ch) = (sampleAddress ? remaining : 0) + (waiting ? sampleCount : 0)
sceAudioOutput2GetRestSample() = (armed descriptor count) * current sampleCount
```
Both channel versions count a parked thread's buffer as well as the one playing. They only
differ after an output with a null pointer, which leaves `remaining` set with no buffer, and the
hardware duly reports `0x400` from one and `0` from the other.
`sceAudioOutput2GetRestSample` reports in units of the *current* length, so after
`sceAudioOutput2ChangeLength(64)` a 4096-sample buffer armed earlier reads back as `0x40`.
## Where the emulator has to fake something
The mixer thread outranks whoever called, so on hardware the first 64 samples of a buffer are
consumed before the output call has returned - a channel reserved for exactly 64 samples is free
again immediately. The emulator's mixer is a timer event instead, so it re-phases that event to
the moment the mixer's DMA starts and runs one block right then. The SRC channel gets the phase
reset but no early read, since its DMA feeds the codec directly. Without this the answer to
`sceAudioGetChannelRestLen` right after an output would depend on where the timer happened to
be, and would differ from run to run.
Two things are still approximate, both below one mix block:
- A descriptor retires when the DMA transfer finishes, which is slightly ahead of the audio
being heard, so hardware frees an Output2 buffer about 100us earlier than the emulator does.
- The emulator only retires SRC buffers on block boundaries, so a buffer whose length is not a
multiple of the block can be up to 1.5ms late. `audio/output2/frequency` and
`audio/output2/rest` are in `tests_next` for this reason.
## Argument checking
Recorded here because several of these were guesses before, and because the *order* of the
checks is observable.
| function | checks, in order |
|---|---|
| `sceAudioChReserve` | the free-channel search runs 7 down to 0 and needs the channel both released and finished playing; bad channel `80260003`; already reserved `80260003`; count not a positive multiple of 64 up to 0xFFC0 `80260006`; format not 0 or 0x10 `80260007` |
| `sceAudioChRelease` | bad channel `80260003`; not reserved `80260008`; a thread parked `80260002`. Clears only the reservation - a buffer already playing plays out. |
| `sceAudioSetChannelDataLen` | bad channel `80260003`; **bad length `80260006`, before the reservation is looked at**; parked thread `80260002`; not reserved `80260001` |
| `sceAudioChangeChannelConfig` | bad channel `80260003`; parked thread or buffer in flight `80260002`; not reserved `80260008`; bad format `80260007` |
| `sceAudioChangeChannelVolume` | either side above 0xFFFF `8026000b`; bad channel `80260003`; **no reservation check**; a negative volume leaves that side alone |
| `sceAudioOutput`, `sceAudioOutputPanned` | each volume compared signed, so negatives pass and mean "leave unchanged" |
| `sceAudioOutputPannedBlocking` | the two volumes are ORed before comparing, so a negative one fails with `8026000b` |
| `sceAudioOutput2ChangeLength` | **length outside 17..4111 `80260006`, before the reservation**; not reserved `80260008` |
| `sceAudioSRCChReserve` | channels not 2 or 4 `80000104`; channels 4 `80000003`; count outside 17..4111 `80000104`; bad frequency `8026000a`; already reserved `80268002` |
| `sceAudioSRCChRelease` | not reserved `80260008`; a descriptor still armed `80268002` |
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
call. It hands the channel a null pointer first - the drain idiom above - and only then does the
ordinary SRC release. So it blocks for one buffer and returns 0 where the other two would refuse
with `80268002`, and whatever was playing is played out rather than cut off. It also ignores its
own reservation flag and releases whatever holds the SRC channel, which is why
`tests/audio/sceaudio/reserve` gets away with what it calls the "wrong release".
`sceVaudioChReserve` marks vaudio reserved *before* delegating and does not undo that when the
delegate fails, so a caller that got `80268002` because Output2 held the channel is told
`80000021` next time round until a release clears it.
## What a call costs
Worth knowing because a game whose losing thread retries in a loop feels it directly.
`tests/audio/blocking/overhead` measures it in buckets:
| call | cost |
|---|---|
| Output2/SRC output, both descriptors already armed | 30-100us |
| Output2/SRC output, any other outcome, including an unreserved channel | over 100us |
| mixer output that is refused, blocking or not | under 10us |
| mixer output that starts the DMA | over 100us |
Every SRC output ends up querying the codec, which is the slow part; only the refused case gets
out before it. Mixer channels are the other way round - cheap unless the call is the one that
brings the DMA up.
## Savestates
`AudioChannel` is at section version 4 and the SRC channel has a section of its own. Anything
older stored a ring of already-mixed samples, which cannot be turned back into a buffer pointer
and a position, so loading one drops the pending audio and wakes any parked threads. That costs
a fraction of a second of silence on load and nothing else. Older states also carry the SRC
channel as a ninth entry in the channel array, which is read and discarded.
+10 -2
View File
@@ -113,7 +113,17 @@ tests_good = [
"audio/mp3/reserve",
"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",
"audio/output2/changelength",
"audio/output2/release",
"audio/output2/reserve",
"audio/output2/threads",
"audio/reverb/basic",
@@ -411,13 +421,11 @@ tests_next = [
"cpu/vfpu/prefixes",
"cpu/vfpu/vector",
"cpu/vfpu/vregs",
"audio/sceaudio/datalen",
"audio/sceaudio/output",
"audio/sceaudio/reserve",
"audio/sascore/setadsr",
"audio/mp3/init",
"audio/output2/frequency",
"audio/output2/release",
"audio/output2/rest",
"ccc/convertstring",
"display/hcount",