mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-30 08:16:38 +02:00
Another pass looking for gaps, all of it now recorded by audio/blocking/channels and audio/blocking/oneshot. sceAudioOneshotOutput was the last unimplemented entry in the module, returning "library not linked" to anyone who called it. It plays one buffer on a channel it never reserves, so the channel frees itself when the buffer runs out, and its argument checks are their own set: any positive sample count, aligned or not, no upper bound, a negative volume rejected rather than skipped, and no busy check at all. No game is known to use it; it is implemented because tracing it turned out to be cheap, not because anything needed it. The channels test confirms four behaviors: sceAudioChReserve(-1) skips a released channel that is still playing while a reserve of it succeeds, a second channel joining a running mixer doesn't lose a block unlike a first, mono counts down in the same 64-sample steps over the same time as stereo, and the panned blocking output has no extra delay on the high channels. Also: the SRC resampler now interpolates into the next buffer at a buffer join instead of holding the last sample, since the codec reads the two descriptors as one stream. That only shows up at non-native rates and no test can see it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
201 lines
11 KiB
Markdown
201 lines
11 KiB
Markdown
# 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.
|