mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
sceAudio: model the real buffering, so contending threads get told "busy"
I deduced that this was the case, and attempted implementing this path long ago, but I could never quite get it to work in all games. Set Claude on a quest to research and implement it, and lo and behold, it works. A bit sobering. Fixes #12888 and likely more. Additionally, audio latency is likely slightly improved overall, and memory usage is down by 4.6MB. Claude says: The blocking output calls are not a queue that callers line up behind. Each mixer channel holds exactly one buffer and at most one parked thread; a second thread arriving while the first is waiting is told the channel is busy and is expected to skip its turn. The Output2/SRC channel holds two DMA descriptors and refuses a third caller outright, without waiting at all. We blocked everyone instead, so a game running a movie thread and a sound-effect thread over one output made the two alternate - a frame of movie audio, a frame of effects silence - and the movie played at half rate. That is #12888, seen in F1 2009 and Colin McRae: DiRT 2. With this, the movie thread keeps the channel for the whole cutscene and the effects thread is refused, which is what the hardware trace shows. The driver also never copies a buffer on the way in: it stores the pointer and its mixer walks it forward 64 samples at a time out of the game's own memory. Modelling that fixes #20095 as a side effect, and drops the 4.6MB of per-channel sample rings we were carrying. The mix event is re-phased to the moment a DMA starts, since the mixer thread outranks its caller and gets a block in before the output call returns. Along the way: the two rest-length calls differ after a null-pointer output, sceAudioChRelease reports not-reserved rather than not-init, sceAudioChangeChannelConfig validates the format, sceAudioChangeChannelVolume validates nothing, and sceAudioOutput2ChangeLength takes a range of 17..4111. Details in docs/sceAudio.md. Savestates: AudioChannel goes to version 4. Older ones stored mixed samples that can't become a pointer and a position again, so they load with the pending audio dropped and any parked threads released. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
6fa654fce2
commit
ac50cd5759
10 files changed
+718
-357
No files matched your search
@@ -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 |
|
||||
|
||||
|
||||
+328
-214
@@ -47,10 +47,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 +68,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 +91,6 @@ void __AudioInit() {
|
||||
mixFrequency = 44100;
|
||||
srcFrequency = 0;
|
||||
|
||||
chanQueueMaxSizeFactor = 2;
|
||||
chanQueueMinSizeFactor = 1;
|
||||
|
||||
__AudioCPUMHzChange();
|
||||
|
||||
eventAudioUpdate = CoreTiming::RegisterEvent("AudioUpdate", &hleAudioUpdate);
|
||||
@@ -188,132 +176,234 @@ 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;
|
||||
|
||||
// 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--);
|
||||
// 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 (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX; i++) {
|
||||
if (g_audioChans[i].sampleAddress != 0) {
|
||||
return true;
|
||||
}
|
||||
// This means the thread stopped waiting, so stop trying to wake it.
|
||||
else if (waitID == 0)
|
||||
chan.waitingThreads.erase(chan.waitingThreads.begin() + w--);
|
||||
}
|
||||
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;
|
||||
}
|
||||
CoreTiming::UnscheduleEvent(eventAudioUpdate, 0);
|
||||
__AudioUpdate();
|
||||
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;
|
||||
}
|
||||
|
||||
if (wokeThreads) {
|
||||
__KernelReSchedule("audio drain");
|
||||
chan.remainingSamples = chan.sampleCount;
|
||||
if (leftVol >= 0) {
|
||||
chan.leftVolume = leftVol;
|
||||
}
|
||||
if (rightVol >= 0) {
|
||||
chan.rightVolume = rightVol;
|
||||
}
|
||||
// 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.
|
||||
const bool wasIdle = !__AudioAnyChannelPlaying();
|
||||
chan.sampleAddress = samplePtr;
|
||||
if (samplePtr != 0 && wasIdle) {
|
||||
__AudioStartMixerDMA();
|
||||
}
|
||||
return chan.sampleCount;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
u32 __AudioSRCEnqueueBlocking(AudioChannel &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.SRCFull()) {
|
||||
return SCE_ERROR_AUDIO_CHANNEL_BUSY;
|
||||
}
|
||||
|
||||
if (vol >= 0) {
|
||||
chan.leftVolume = vol;
|
||||
chan.rightVolume = vol;
|
||||
}
|
||||
|
||||
u32 result = 0;
|
||||
if (samplePtr != 0) {
|
||||
if (chan.srcBufferCount == 0) {
|
||||
// Starting the DMA signals a completion by itself, which is why the first
|
||||
// output after an idle stretch returns without blocking.
|
||||
chan.srcCompletion = true;
|
||||
chan.srcPlayedSamples = 0;
|
||||
chan.srcFrac = 0;
|
||||
}
|
||||
const bool wasIdle = chan.srcBufferCount == 0;
|
||||
chan.srcBuffers[chan.srcBufferCount].address = samplePtr;
|
||||
chan.srcBuffers[chan.srcBufferCount].samples = chan.sampleCount;
|
||||
chan.srcBufferCount++;
|
||||
result = chan.sampleCount;
|
||||
if (wasIdle) {
|
||||
__AudioStartSRCDMA();
|
||||
}
|
||||
} else if (chan.srcBufferCount == 0) {
|
||||
// Nothing playing and nothing handed over, so there is no completion to wait for.
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (chan.srcCompletion) {
|
||||
chan.srcCompletion = false;
|
||||
return result;
|
||||
}
|
||||
if (!__KernelIsDispatchEnabled()) {
|
||||
return SCE_KERNEL_ERROR_CAN_NOT_WAIT;
|
||||
}
|
||||
|
||||
chan.srcWaitingThreads.push_back(__KernelGetCurThread());
|
||||
__KernelWaitCurThread(WAITTYPE_AUDIOCHANNEL, (SceUID)chan.index + 1, result, 0, false, "blocking audio");
|
||||
return result;
|
||||
}
|
||||
|
||||
void __AudioSRCSignal(AudioChannel &chan) {
|
||||
chan.srcCompletion = true;
|
||||
}
|
||||
|
||||
// One of the two SRC buffers finished playing.
|
||||
static bool __AudioSRCCompleted(AudioChannel &chan) {
|
||||
if (chan.srcWaitingThreads.empty()) {
|
||||
// Nobody is listening, so the completion sits there for the next caller to consume.
|
||||
chan.srcCompletion = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const SceUID threadID = chan.srcWaitingThreads.front();
|
||||
chan.srcWaitingThreads.erase(chan.srcWaitingThreads.begin());
|
||||
|
||||
u32 error;
|
||||
if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) == 0) {
|
||||
return false;
|
||||
}
|
||||
__KernelResumeThreadFromWait(threadID, __KernelGetWaitValue(threadID, error));
|
||||
return true;
|
||||
}
|
||||
|
||||
void __AudioWakeThreads(AudioChannel &chan, int result) {
|
||||
__AudioWakeThreads(chan, result, 0x7FFFFFFF);
|
||||
bool woke = false;
|
||||
|
||||
if (chan.waitingThread != 0) {
|
||||
const SceUID threadID = chan.waitingThread;
|
||||
chan.waitingThread = 0;
|
||||
u32 error;
|
||||
if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) != 0) {
|
||||
__KernelResumeThreadFromWait(threadID, result);
|
||||
woke = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (SceUID threadID : chan.srcWaitingThreads) {
|
||||
u32 error;
|
||||
if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) != 0) {
|
||||
__KernelResumeThreadFromWait(threadID, result);
|
||||
woke = true;
|
||||
}
|
||||
}
|
||||
chan.srcWaitingThreads.clear();
|
||||
|
||||
if (woke) {
|
||||
__KernelReSchedule("audio drain");
|
||||
}
|
||||
}
|
||||
|
||||
void __AudioSetOutputFrequency(int freq) {
|
||||
@@ -329,101 +419,125 @@ 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 || chan.remainingSamples == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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(AudioChannel &chan) {
|
||||
if (chan.srcBufferCount == 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);
|
||||
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.srcBufferCount == 0) {
|
||||
// Underrun. The rest of the block stays silent, like a descriptor the game
|
||||
// never got around to arming.
|
||||
break;
|
||||
}
|
||||
|
||||
const AudioPendingBuffer &buf = chan.srcBuffers[0];
|
||||
const u32 addr = buf.address + chan.srcPlayedSamples * 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.srcPlayedSamples, 2u);
|
||||
if (!chan.mute && Memory::IsValidRange(addr, avail * 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;
|
||||
// 15 bits of fraction, not 16 - a full 16 would overflow the product against a
|
||||
// full-scale difference.
|
||||
const int frac = (int)(chan.srcFrac >> 1);
|
||||
mixBuffer[out * 2] += ApplyChannelVolume(l0 + (((l1 - l0) * frac) >> 15), leftVol);
|
||||
mixBuffer[out * 2 + 1] += ApplyChannelVolume(r0 + (((r1 - r0) * frac) >> 15), rightVol);
|
||||
}
|
||||
|
||||
chan.srcFrac += ratio;
|
||||
u32 step = chan.srcFrac >> 16;
|
||||
chan.srcFrac &= 0xFFFF;
|
||||
while (step > 0 && chan.srcBufferCount > 0) {
|
||||
const u32 take = std::min(step, chan.srcBuffers[0].samples - chan.srcPlayedSamples);
|
||||
chan.srcPlayedSamples += take;
|
||||
step -= take;
|
||||
if (chan.srcPlayedSamples >= chan.srcBuffers[0].samples) {
|
||||
chan.srcBuffers[0] = chan.srcBuffers[1];
|
||||
chan.srcBufferCount--;
|
||||
chan.srcPlayedSamples = 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 (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX; i++) {
|
||||
// Deliberately not gated on `reserved`: sceAudioChRelease only clears the
|
||||
// reservation, and a buffer already in flight keeps playing out.
|
||||
woke |= __AudioMixChannel(g_audioChans[i]);
|
||||
}
|
||||
woke |= __AudioMixSRC(g_audioChans[PSP_AUDIO_CHANNEL_SRC]);
|
||||
audioMixing = false;
|
||||
|
||||
if (firstChannel) {
|
||||
// Nothing was written above, let's memset.
|
||||
memset(mixBuffer, 0, hwBlockSize * 2 * sizeof(s32));
|
||||
if (woke) {
|
||||
__KernelReSchedule("audio drain");
|
||||
}
|
||||
|
||||
if (g_Config.bEnableSound) {
|
||||
|
||||
+16
-3
@@ -41,9 +41,22 @@ 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);
|
||||
// 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);
|
||||
// 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(AudioChannel &chan, u32 samplePtr, int vol);
|
||||
// Hands the SRC channel a completion that the next caller can consume without waiting.
|
||||
void __AudioSRCSignal(AudioChannel &chan);
|
||||
|
||||
// Wakes whoever is parked on the channel with the given error, and forgets their buffer.
|
||||
void __AudioWakeThreads(AudioChannel &chan, int result);
|
||||
|
||||
void __AudioCPUMHzChange();
|
||||
|
||||
+153
-120
@@ -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,17 @@ 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];
|
||||
|
||||
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 +58,115 @@ 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);
|
||||
Do(p, srcBufferCount);
|
||||
for (AudioPendingBuffer &buf : srcBuffers) {
|
||||
Do(p, buf.address);
|
||||
Do(p, buf.samples);
|
||||
}
|
||||
Do(p, srcPlayedSamples);
|
||||
Do(p, srcFrac);
|
||||
Do(p, srcCompletion);
|
||||
Do(p, srcWaitingThreads);
|
||||
Do(p, defaultRoutingMode);
|
||||
Do(p, defaultRoutingVolMode);
|
||||
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) {
|
||||
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) {
|
||||
sampleAddress = 0;
|
||||
remainingSamples = 0;
|
||||
waitingThread = 0;
|
||||
waitingAddress = 0;
|
||||
srcBufferCount = 0;
|
||||
srcPlayedSamples = 0;
|
||||
srcFrac = 0;
|
||||
srcCompletion = false;
|
||||
srcWaitingThreads.clear();
|
||||
// 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()
|
||||
{
|
||||
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;
|
||||
srcBufferCount = 0;
|
||||
srcPlayedSamples = 0;
|
||||
srcFrac = 0;
|
||||
srcCompletion = false;
|
||||
srcWaitingThreads.clear();
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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 +174,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 +184,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 +237,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 +259,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 +290,32 @@ 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) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -295,6 +339,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,27 +353,25 @@ 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)
|
||||
u32 result = __AudioSRCEnqueueBlocking(g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2], dataPtr, vol);
|
||||
if ((int)result < 0)
|
||||
return hleLogError(Log::sceAudio, result);
|
||||
return hleLogDebug(Log::sceAudio, result);
|
||||
}
|
||||
|
||||
static u32 sceAudioOutput2ChangeLength(u32 sampleCount) {
|
||||
// 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_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2];
|
||||
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);
|
||||
}
|
||||
@@ -338,23 +381,20 @@ static u32 sceAudioOutput2GetRestSample() {
|
||||
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.srcBufferCount * chan.sampleCount);
|
||||
}
|
||||
|
||||
static u32 sceAudioOutput2Release() {
|
||||
auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2];
|
||||
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.srcBufferCount != 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);
|
||||
}
|
||||
|
||||
@@ -399,6 +439,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;
|
||||
@@ -411,11 +452,12 @@ static u32 sceAudioSRCChRelease() {
|
||||
auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_SRC];
|
||||
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.srcBufferCount != 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,18 +466,9 @@ 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)
|
||||
u32 result = __AudioSRCEnqueueBlocking(g_audioChans[PSP_AUDIO_CHANNEL_SRC], buf, vol);
|
||||
if ((int)result < 0)
|
||||
return hleLogError(Log::sceAudio, result);
|
||||
return hleLogDebug(Log::sceAudio, result);
|
||||
}
|
||||
@@ -531,7 +564,7 @@ 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" },
|
||||
|
||||
+39
-8
@@ -17,7 +17,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
|
||||
#include "CommonTypes.h"
|
||||
#include "sceKernel.h"
|
||||
@@ -33,18 +33,21 @@ const int PSP_AUDIO_CHANNEL_SRC = 8;
|
||||
const int PSP_AUDIO_CHANNEL_OUTPUT2 = 8;
|
||||
const int PSP_AUDIO_CHANNEL_VAUDIO = 8;
|
||||
|
||||
struct AudioChannelWaitInfo {
|
||||
SceUID threadID;
|
||||
int numSamples;
|
||||
// One buffer handed over and not yet fully played.
|
||||
struct AudioPendingBuffer {
|
||||
u32 address;
|
||||
u32 samples;
|
||||
};
|
||||
|
||||
// Mirrors the 16-byte channel struct the audio driver keeps, plus the two DMA descriptors it
|
||||
// uses for the SRC/Output2 channel. See docs/sceAudio.md for how the hardware behaves; the
|
||||
// short version is that it plays out of the game's own memory rather than taking a copy, and
|
||||
// that it holds one buffer per mixer channel and two for SRC.
|
||||
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,12 +55,40 @@ struct AudioChannel {
|
||||
// For the debugger only. Not saved.
|
||||
bool mute = false;
|
||||
|
||||
std::vector<AudioChannelWaitInfo> waitingThreads;
|
||||
// Channels 0-7. 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;
|
||||
|
||||
// Only one thread can be parked in a blocking output call on a channel. A second one is
|
||||
// told the channel is busy rather than queueing up behind the first. These remember what
|
||||
// it wanted to hand over, so the enqueue can be retried when the buffer finishes.
|
||||
SceUID waitingThread = 0;
|
||||
u32 waitingAddress = 0;
|
||||
int waitingLeftVolume = 0;
|
||||
int waitingRightVolume = 0;
|
||||
|
||||
// Channel 8 (Output2/SRC/Vaudio) instead has two DMA descriptors, so two buffers can be
|
||||
// in flight at once and the third caller is turned away.
|
||||
AudioPendingBuffer srcBuffers[2]{};
|
||||
int srcBufferCount = 0;
|
||||
u32 srcPlayedSamples = 0; // Consumed from srcBuffers[0].
|
||||
u32 srcFrac = 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 srcCompletion = false;
|
||||
std::vector<SceUID> srcWaitingThreads;
|
||||
|
||||
void DoState(PointerWrap &p);
|
||||
|
||||
void reset();
|
||||
void clear();
|
||||
|
||||
bool SRCFull() const {
|
||||
return srcBufferCount >= (int)ARRAY_SIZE(srcBuffers);
|
||||
}
|
||||
};
|
||||
|
||||
// The extra channel is for SRC/Output2/Vaudio (who all share, apparently.)
|
||||
|
||||
@@ -69,6 +69,7 @@ static u32 sceVaudioChReserve(int sampleCount, int freq, int 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].clear();
|
||||
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;
|
||||
@@ -85,7 +86,6 @@ static u32 sceVaudioChRelease() {
|
||||
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;
|
||||
}
|
||||
@@ -93,11 +93,8 @@ static u32 sceVaudioChRelease() {
|
||||
|
||||
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_audioChans[PSP_AUDIO_CHANNEL_VAUDIO], buffer, vol);
|
||||
}
|
||||
|
||||
static u32 sceVaudioSetEffectType(int effectType, int vol) {
|
||||
|
||||
@@ -1666,10 +1666,22 @@ void DrawAudioChannels(ImConfig &cfg, ImControl &control) {
|
||||
break;
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
for (auto t : g_audioChans[i].waitingThreads) {
|
||||
KernelObject *thread = kernelObjects.GetFast<KernelObject>(t.threadID);
|
||||
// At most one thread can be parked on a mixer channel; the SRC channel keeps its
|
||||
// own list instead.
|
||||
SceUID waiting[3];
|
||||
int waitingCount = 0;
|
||||
if (g_audioChans[i].waitingThread != 0) {
|
||||
waiting[waitingCount++] = g_audioChans[i].waitingThread;
|
||||
}
|
||||
for (SceUID t : g_audioChans[i].srcWaitingThreads) {
|
||||
if (waitingCount < ARRAY_SIZE(waiting)) {
|
||||
waiting[waitingCount++] = t;
|
||||
}
|
||||
}
|
||||
for (int w = 0; w < waitingCount; w++) {
|
||||
KernelObject *thread = kernelObjects.GetFast<KernelObject>(waiting[w]);
|
||||
if (thread) {
|
||||
ImGui::Text("%s: %d", thread->GetName(), t.numSamples);
|
||||
ImGui::TextUnformatted(thread->GetName());
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# 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.
|
||||
|
||||
**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.
|
||||
|
||||
## Savestates
|
||||
|
||||
`AudioChannel` is at section version 4. 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.
|
||||
+1
-1
Submodule pspautotests updated: 1dda6a3709...3d55f06ed5.
@@ -113,7 +113,13 @@ tests_good = [
|
||||
"audio/mp3/reserve",
|
||||
"audio/mp3/setloopnum",
|
||||
"audio/mp3/stream",
|
||||
"audio/blocking/contend",
|
||||
"audio/blocking/depth",
|
||||
"audio/blocking/errors",
|
||||
"audio/blocking/restlen",
|
||||
"audio/sceaudio/datalen",
|
||||
"audio/output2/changelength",
|
||||
"audio/output2/release",
|
||||
"audio/output2/reserve",
|
||||
"audio/output2/threads",
|
||||
"audio/reverb/basic",
|
||||
@@ -411,13 +417,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",
|
||||
|
||||
Reference in new issue
Block a user