Clean up audio subsystem duplication, implement seeking and loop regions
- Extract audioStreamGetPanFactors() into the shared layer, replacing
identical pan-to-LR math duplicated in the PSP and Dolphin backends
(and dropping a dead clamp - directionality's int8_t range already
guarantees pan stays in [-1, 1]).
- Implement audioStreamSetPosition() for real (was previously a no-op
that computed a value and threw it away) and add
audioStreamSetLoopPoints() to actually drive loopStart/loopTo, which
were previously dead fields with no setter at all. Both are threaded
through all three platform backends:
- PSP: the feeder thread now bounds each pass by the loop segment
and wraps to loopTo instead of always frame 0, while still filling
hardware chunks gaplessly.
- Dolphin: loopTo/loopStart map directly onto ansnd's existing
loop_start_offset/loop_end_offset, and startFrame onto start_offset.
- Linux: Buffer() now queues only the current segment, clearing the
SDL queue on an explicit seek but preserving the existing
overlap-based gapless loop restart otherwise.
- Linux: skip the mix scratch-buffer entirely at full volume, queuing
stream->data directly instead of allocating/zeroing/copying into one
every buffer call for no reason.
Verified no regression in the default (no seek, no loop points) case on
Linux/PPSSPP/Dolphin (-a LLE), and verified seek + loop-region behavior
manually via a temporary engine.c smoke-test tweak (reverted) showing
the expected faster loop cadence and seek-then-loop sequencing.
This commit is contained in:
@@ -19,6 +19,8 @@ errorret_t audioStreamInit(audiostream_t *stream) {
|
|||||||
stream->loopStart = -1;
|
stream->loopStart = -1;
|
||||||
stream->loopTo = 0;
|
stream->loopTo = 0;
|
||||||
stream->duration = 0;
|
stream->duration = 0;
|
||||||
|
stream->startFrame = 0;
|
||||||
|
stream->seeking = false;
|
||||||
stream->user = NULL;
|
stream->user = NULL;
|
||||||
stream->onLoop = NULL;
|
stream->onLoop = NULL;
|
||||||
stream->onEnd = NULL;
|
stream->onEnd = NULL;
|
||||||
@@ -43,16 +45,53 @@ void audioStreamPause(audiostream_t *stream) {
|
|||||||
|
|
||||||
void audioStreamSetPosition(audiostream_t *stream, const float_t position) {
|
void audioStreamSetPosition(audiostream_t *stream, const float_t position) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
// Clamp time.
|
// Wrap into [0, duration).
|
||||||
float_t t = position;
|
float_t t = position;
|
||||||
while(t < 0) t += stream->duration;
|
while(t < 0) t += stream->duration;
|
||||||
while(t >= stream->duration) t -= stream->duration;
|
while(t >= stream->duration) t -= stream->duration;
|
||||||
|
|
||||||
// Here I will set the stream time, this will require new data to be fetched
|
stream->startFrame = (size_t) (t * stream->pcm.sampleRate);
|
||||||
// next update() call.
|
stream->seeking = true;
|
||||||
// To confirm: If this is called late in the frame, will the PCM data be sent
|
|
||||||
// to the output despite time being adjusted? is this going to cause static?
|
// Force the next audioStreamUpdate() to re-buffer from startFrame instead
|
||||||
|
// of continuing whatever was already buffered - see the platform Buffer()
|
||||||
|
// implementations for how each one applies startFrame; PSP is the one
|
||||||
|
// exception (see audioStreamSetPosition's own doc comment).
|
||||||
|
stream->state &= ~AUDIO_STREAM_STATE_BUFFERED;
|
||||||
|
}
|
||||||
|
|
||||||
|
void audioStreamSetLoopPoints(
|
||||||
|
audiostream_t *stream,
|
||||||
|
const float_t loopStart,
|
||||||
|
const float_t loopTo
|
||||||
|
) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
assertTrue(
|
||||||
|
loopStart < 0 || loopTo < loopStart,
|
||||||
|
"loopTo must be before loopStart."
|
||||||
|
);
|
||||||
|
|
||||||
|
stream->loopStart = loopStart;
|
||||||
|
stream->loopTo = loopTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
void audioStreamGetPanFactors(
|
||||||
|
const audiostream_t *stream,
|
||||||
|
float_t *outLeft,
|
||||||
|
float_t *outRight
|
||||||
|
) {
|
||||||
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
assertNotNull(outLeft, "outLeft cannot be NULL.");
|
||||||
|
assertNotNull(outRight, "outRight cannot be NULL.");
|
||||||
|
|
||||||
|
// directionality is an int8_t clamped to AUDIO_STREAM_LEFT..RIGHT
|
||||||
|
// (-128..127) by its own type, so pan is always within [-1.0, 0.992] -
|
||||||
|
// no further clamping needed.
|
||||||
|
const float_t pan = (float_t) stream->directionality / 128.0f;
|
||||||
|
|
||||||
|
*outLeft = pan > 0 ? (1.0f - pan) : 1.0f;
|
||||||
|
*outRight = pan < 0 ? (1.0f + pan) : 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void audioStreamSetVolume(audiostream_t *stream, const uint8_t volume) {
|
void audioStreamSetVolume(audiostream_t *stream, const uint8_t volume) {
|
||||||
@@ -103,10 +142,7 @@ errorret_t audioStreamUpdate(audiostream_t *stream) {
|
|||||||
|
|
||||||
// TODO: Once streamed (rather than fully in-memory) sources exist, this is
|
// TODO: Once streamed (rather than fully in-memory) sources exist, this is
|
||||||
// where new data would be decoded/read in and re-buffered as playback
|
// where new data would be decoded/read in and re-buffered as playback
|
||||||
// consumes it. Looping currently only supports restarting from the very
|
// consumes it.
|
||||||
// start of the buffer (loopTo is not yet honored) since replaying the
|
|
||||||
// exact same stream->data/dataSize needs no platform-specific changes;
|
|
||||||
// an arbitrary loopTo offset would need slicing that buffer instead.
|
|
||||||
// Checked in this order (finished-check before needs-buffering) so that a
|
// Checked in this order (finished-check before needs-buffering) so that a
|
||||||
// loop restart falls straight through into re-buffering within this same
|
// loop restart falls straight through into re-buffering within this same
|
||||||
// call, instead of leaving BUFFERED cleared for the caller to notice and
|
// call, instead of leaving BUFFERED cleared for the caller to notice and
|
||||||
@@ -120,6 +156,12 @@ errorret_t audioStreamUpdate(audiostream_t *stream) {
|
|||||||
if(stream->state & AUDIO_STREAM_STATE_LOOPING) {
|
if(stream->state & AUDIO_STREAM_STATE_LOOPING) {
|
||||||
stream->state &= ~AUDIO_STREAM_STATE_BUFFERED;
|
stream->state &= ~AUDIO_STREAM_STATE_BUFFERED;
|
||||||
|
|
||||||
|
// Resume from loopTo rather than the very start of the buffer - only
|
||||||
|
// matters for platforms that reach this generic restart path at all;
|
||||||
|
// PSP/Dolphin loop entirely on their own (thread/hardware) and never
|
||||||
|
// report "finished" while looping, so in practice this is Linux-only.
|
||||||
|
stream->startFrame = (size_t) (stream->loopTo * stream->pcm.sampleRate);
|
||||||
|
|
||||||
if(stream->onLoop != NULL) {
|
if(stream->onLoop != NULL) {
|
||||||
stream->onLoop(stream);
|
stream->onLoop(stream);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,15 +63,40 @@ typedef struct audiostream_s {
|
|||||||
// TODO: Can we use a 3D vector + Pro Logic II?
|
// TODO: Can we use a 3D vector + Pro Logic II?
|
||||||
int8_t directionality;
|
int8_t directionality;
|
||||||
|
|
||||||
// At what point does the stream loop back, -1 means "at end of stream"
|
// In seconds, where the loop segment ends and playback jumps back to
|
||||||
|
// loopTo - -1 (the default) means "at the end of the stream", i.e. loop
|
||||||
|
// the whole buffer. Set via audioStreamSetLoopPoints().
|
||||||
float_t loopStart;
|
float_t loopStart;
|
||||||
|
|
||||||
// When a loop occurs, where do we loop to? Defaults to 0 or start of stream.
|
// In seconds, where a loop jumps back to once it reaches loopStart.
|
||||||
|
// Defaults to 0 (start of stream). Set via audioStreamSetLoopPoints().
|
||||||
float_t loopTo;
|
float_t loopTo;
|
||||||
|
|
||||||
// Cached duration of the stream in seconds.
|
// Cached duration of the stream in seconds.
|
||||||
float_t duration;
|
float_t duration;
|
||||||
|
|
||||||
|
// Frame offset the next platform Buffer() call should start playback
|
||||||
|
// from - set by audioStreamSetPosition() (an explicit seek) and by
|
||||||
|
// audioStreamUpdate() itself (to loopTo's frame offset, for platforms
|
||||||
|
// that re-enter Buffer() on every loop restart rather than looping
|
||||||
|
// natively). Each platform's Buffer()-invoking entry point must read and
|
||||||
|
// reset this to 0 synchronously, in the same call that decided to
|
||||||
|
// (re)buffer - not later/asynchronously (e.g. from a feeder thread),
|
||||||
|
// since audioStreamUpdate() may already have moved on to something else
|
||||||
|
// that touches this field by the time an async reader gets to it.
|
||||||
|
size_t startFrame;
|
||||||
|
|
||||||
|
// True when startFrame came from an explicit audioStreamSetPosition()
|
||||||
|
// seek rather than a natural loop restart. Platforms whose Buffer() call
|
||||||
|
// can leave previously-queued audio still playing underneath the new
|
||||||
|
// pass (currently just Linux's SDL queue, kept deliberately overlapping
|
||||||
|
// across a loop restart to avoid a gap) need this to tell "jump now,
|
||||||
|
// discarding whatever's still playing" (seek) apart from "let the old
|
||||||
|
// tail keep playing while the new pass queues underneath it" (loop
|
||||||
|
// restart) - both look identical as just "a pending startFrame"
|
||||||
|
// otherwise. Consumed (reset to false) the same way as startFrame.
|
||||||
|
bool_t seeking;
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
void *user;
|
void *user;
|
||||||
void (*onLoop)(audiostream_t *stream);
|
void (*onLoop)(audiostream_t *stream);
|
||||||
@@ -128,12 +153,50 @@ void audioStreamPlay(audiostream_t *stream);
|
|||||||
void audioStreamPause(audiostream_t *stream);
|
void audioStreamPause(audiostream_t *stream);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the playback position of the given audio stream, in seconds.
|
* Seeks the given audio stream to the given position, in seconds, wrapping
|
||||||
|
* into range if the position is outside [0, duration). Takes effect on the
|
||||||
|
* next audioStreamUpdate() call for platforms that re-buffer per frame
|
||||||
|
* (e.g. Linux); on PSP, which manages an entire playback pass on its own
|
||||||
|
* feeder thread once started, a seek only takes effect the next time the
|
||||||
|
* stream begins playing from a stopped state, not instantaneously mid-pass.
|
||||||
*
|
*
|
||||||
* @param stream The audio stream to rewind.
|
* @param stream The audio stream to seek.
|
||||||
|
* @param position The new playback position, in seconds.
|
||||||
*/
|
*/
|
||||||
void audioStreamSetPosition(audiostream_t *stream, const float_t position);
|
void audioStreamSetPosition(audiostream_t *stream, const float_t position);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the loop region for the given audio stream: once playback reaches
|
||||||
|
* loopStart, it jumps back to loopTo instead of continuing (or stopping,
|
||||||
|
* if not looping). Has no effect unless looping is also enabled via
|
||||||
|
* audioStreamSetLooping().
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to update.
|
||||||
|
* @param loopStart Where the loop segment ends, in seconds, or -1 to loop
|
||||||
|
* the whole stream (the default).
|
||||||
|
* @param loopTo Where the loop segment starts, in seconds.
|
||||||
|
*/
|
||||||
|
void audioStreamSetLoopPoints(
|
||||||
|
audiostream_t *stream,
|
||||||
|
const float_t loopStart,
|
||||||
|
const float_t loopTo
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes normalized left/right pan factors (0..1) for the given audio
|
||||||
|
* stream's directionality. Callers multiply these by their own
|
||||||
|
* platform-specific base volume representation.
|
||||||
|
*
|
||||||
|
* @param stream The audio stream to read directionality from.
|
||||||
|
* @param outLeft Set to the left channel's pan factor.
|
||||||
|
* @param outRight Set to the right channel's pan factor.
|
||||||
|
*/
|
||||||
|
void audioStreamGetPanFactors(
|
||||||
|
const audiostream_t *stream,
|
||||||
|
float_t *outLeft,
|
||||||
|
float_t *outRight
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the playback volume of the given audio stream.
|
* Sets the playback volume of the given audio stream.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -42,10 +42,8 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
|
|||||||
|
|
||||||
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
|
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
|
||||||
|
|
||||||
// Simple linear pan; AUDIO_STREAM_LEFT/CENTER/RIGHT map to -128..0..127.
|
float_t leftFactor, rightFactor;
|
||||||
float_t pan = (float_t) stream->directionality / 128.0f;
|
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
||||||
if(pan < -1.0f) pan = -1.0f;
|
|
||||||
if(pan > 1.0f) pan = 1.0f;
|
|
||||||
|
|
||||||
const float_t baseVolume = (float_t) stream->volume / 255.0f;
|
const float_t baseVolume = (float_t) stream->volume / 255.0f;
|
||||||
|
|
||||||
@@ -57,8 +55,8 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
|
|||||||
config.format = ANSND_VOICE_PCM_FORMAT_SIGNED_16_PCM;
|
config.format = ANSND_VOICE_PCM_FORMAT_SIGNED_16_PCM;
|
||||||
config.channels = stream->pcm.channels;
|
config.channels = stream->pcm.channels;
|
||||||
config.pitch = 1.0f;
|
config.pitch = 1.0f;
|
||||||
config.left_volume = baseVolume * (pan > 0 ? (1.0f - pan) : 1.0f);
|
config.left_volume = baseVolume * leftFactor;
|
||||||
config.right_volume = baseVolume * (pan < 0 ? (1.0f + pan) : 1.0f);
|
config.right_volume = baseVolume * rightFactor;
|
||||||
// The DSP DMAs frame_data_ptr directly out of main memory, bypassing the
|
// The DSP DMAs frame_data_ptr directly out of main memory, bypassing the
|
||||||
// CPU cache, and rejects any pointer in cached virtual address space
|
// CPU cache, and rejects any pointer in cached virtual address space
|
||||||
// (0x8xxxxxxx.. - checked as "negative" internally, returned as
|
// (0x8xxxxxxx.. - checked as "negative" internally, returned as
|
||||||
@@ -68,28 +66,31 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
|
|||||||
DCFlushRange(stream->data, stream->dataSize);
|
DCFlushRange(stream->data, stream->dataSize);
|
||||||
config.frame_data_ptr = MEM_VIRTUAL_TO_PHYSICAL(stream->data);
|
config.frame_data_ptr = MEM_VIRTUAL_TO_PHYSICAL(stream->data);
|
||||||
config.frame_count = (u32) (stream->dataSize / frameSize);
|
config.frame_count = (u32) (stream->dataSize / frameSize);
|
||||||
|
config.start_offset = (u32) stream->startFrame;
|
||||||
|
stream->startFrame = 0;
|
||||||
|
stream->seeking = false;
|
||||||
config.voice_callback = audioStreamDolphinVoiceCallback;
|
config.voice_callback = audioStreamDolphinVoiceCallback;
|
||||||
config.stream_callback = NULL; // single-buffer playback
|
config.stream_callback = NULL; // single-buffer playback
|
||||||
config.user_pointer = stream;
|
config.user_pointer = stream;
|
||||||
|
|
||||||
// Loop entirely in hardware rather than mirroring PSP/Linux's
|
// Loop entirely in hardware rather than mirroring PSP/Linux's
|
||||||
// detect-finished-then-restart-from-software approach: ansnd's DSP mixer
|
// detect-finished-then-restart-from-software approach: ansnd's DSP mixer
|
||||||
// wraps frame_count-1 back to 0 on its own, so there's no restart latency
|
// wraps loop_end_offset back to loop_start_offset on its own, so there's
|
||||||
// to create a gap in the first place (unlike a software restart, which
|
// no restart latency to create a gap in the first place (unlike a
|
||||||
// always costs at least a little). Only restart-from-the-very-start is
|
// software restart, which always costs at least a little).
|
||||||
// supported elsewhere in this codebase (loopTo isn't honored yet), which
|
|
||||||
// matches exactly what loop_start_offset=0 gives here.
|
|
||||||
//
|
//
|
||||||
// Trade-off: onLoop never fires for a Dolphin voice looping this way -
|
// Trade-off: onLoop never fires for a Dolphin voice looping this way -
|
||||||
// there's no ANSND_VOICE_STATE for "wrapped", only state transitions like
|
// there's no ANSND_VOICE_STATE for "wrapped", only state transitions like
|
||||||
// FINISHED/STOPPED, which a looping voice never reaches. And unlike
|
// FINISHED/STOPPED, which a looping voice never reaches. And unlike
|
||||||
// PSP's per-restart tail fade, the DSP does no smoothing at the wrap
|
// PSP's per-restart tail fade, the DSP does no smoothing at the wrap
|
||||||
// point - it requires the source data's last frame to already flow
|
// point - it requires loopStart's frame to already flow cleanly into
|
||||||
// cleanly into its first (true today only because the shared 441Hz test
|
// loopTo's (true today only because the shared 441Hz test tone was
|
||||||
// tone was deliberately chosen to divide evenly into the sample rate).
|
// deliberately chosen to divide evenly into the sample rate).
|
||||||
if(stream->state & AUDIO_STREAM_STATE_LOOPING) {
|
if(stream->state & AUDIO_STREAM_STATE_LOOPING) {
|
||||||
config.loop_start_offset = 0;
|
config.loop_start_offset = (u32) (stream->loopTo * stream->pcm.sampleRate);
|
||||||
config.loop_end_offset = config.frame_count - 1;
|
config.loop_end_offset = stream->loopStart >= 0
|
||||||
|
? (u32) (stream->loopStart * stream->pcm.sampleRate) - 1
|
||||||
|
: config.frame_count - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
s32 result = ansnd_configure_pcm_voice((u32) stream->platform.voiceId, &config);
|
s32 result = ansnd_configure_pcm_voice((u32) stream->platform.voiceId, &config);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "audio/audiostream.h"
|
#include "audio/audiostream.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
#include "util/math.h"
|
||||||
|
|
||||||
// How many frames of lead time to keep queued ahead of playback. Matches
|
// How many frames of lead time to keep queued ahead of playback. Matches
|
||||||
// SDL_AudioSpec.samples below - the device's own internal buffer size - so
|
// SDL_AudioSpec.samples below - the device's own internal buffer size - so
|
||||||
@@ -44,17 +45,56 @@ errorret_t audioStreamLinuxDispose(audiostream_t *stream) {
|
|||||||
errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
uint8_t *mixed = memoryAllocate(stream->dataSize);
|
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
|
||||||
memoryZero(mixed, stream->dataSize);
|
const size_t totalFrames = stream->dataSize / frameSize;
|
||||||
SDL_MixAudioFormat(
|
|
||||||
mixed, stream->data, AUDIO_S16SYS, (Uint32) stream->dataSize,
|
|
||||||
(stream->volume * SDL_MIX_MAXVOLUME) / 0xFF
|
|
||||||
);
|
|
||||||
|
|
||||||
int queued = SDL_QueueAudio(
|
// Consumed synchronously (right here, in the same call that decided to
|
||||||
stream->platform.device, mixed, (Uint32) stream->dataSize
|
// (re)buffer) rather than left for later - see startFrame's own comment.
|
||||||
);
|
const size_t startFrame = mathMin(stream->startFrame, totalFrames);
|
||||||
memoryFree(mixed);
|
const bool_t seeking = stream->seeking;
|
||||||
|
stream->startFrame = 0;
|
||||||
|
stream->seeking = false;
|
||||||
|
|
||||||
|
size_t endFrame = totalFrames;
|
||||||
|
if((stream->state & AUDIO_STREAM_STATE_LOOPING) && stream->loopStart >= 0) {
|
||||||
|
endFrame = mathMin(
|
||||||
|
(size_t) (stream->loopStart * stream->pcm.sampleRate), totalFrames
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// A seek (or a loop restart landing exactly on the loop end) can put
|
||||||
|
// startFrame at or past endFrame - e.g. seeking into an outro after the
|
||||||
|
// loop point. Play out to the true end of the buffer once instead, same
|
||||||
|
// as PSP.
|
||||||
|
if(startFrame >= endFrame) endFrame = totalFrames;
|
||||||
|
|
||||||
|
const size_t bytes = (endFrame - startFrame) * frameSize;
|
||||||
|
const uint8_t *segment = stream->data + (startFrame * frameSize);
|
||||||
|
|
||||||
|
// Only an explicit seek discards whatever's still queued and jumps -
|
||||||
|
// a natural loop restart deliberately leaves the previous pass's tail
|
||||||
|
// (AUDIO_LINUX_LEAD_FRAMES worth) queued and appends the new pass after
|
||||||
|
// it, which is what makes looping gapless (see IsFinished()'s comment).
|
||||||
|
// Clearing on every Buffer() call would destroy that overlap and
|
||||||
|
// reintroduce the exact gap this was built to avoid.
|
||||||
|
if(seeking) {
|
||||||
|
SDL_ClearQueuedAudio(stream->platform.device);
|
||||||
|
}
|
||||||
|
|
||||||
|
int queued;
|
||||||
|
if(stream->volume == 0xFF) {
|
||||||
|
// Nothing to mix at full volume - queue the segment directly instead of
|
||||||
|
// allocating/zeroing a same-size scratch buffer just to copy it in.
|
||||||
|
queued = SDL_QueueAudio(stream->platform.device, segment, (Uint32) bytes);
|
||||||
|
} else {
|
||||||
|
uint8_t *mixed = memoryAllocate(bytes);
|
||||||
|
memoryZero(mixed, bytes);
|
||||||
|
SDL_MixAudioFormat(
|
||||||
|
mixed, segment, AUDIO_S16SYS, (Uint32) bytes,
|
||||||
|
(stream->volume * SDL_MIX_MAXVOLUME) / 0xFF
|
||||||
|
);
|
||||||
|
queued = SDL_QueueAudio(stream->platform.device, mixed, (Uint32) bytes);
|
||||||
|
memoryFree(mixed);
|
||||||
|
}
|
||||||
|
|
||||||
if(queued != 0) {
|
if(queued != 0) {
|
||||||
errorThrow("Failed to queue SDL2 audio data: %s", SDL_GetError());
|
errorThrow("Failed to queue SDL2 audio data: %s", SDL_GetError());
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "audio/audiostream.h"
|
#include "audio/audiostream.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
#include "util/math.h"
|
||||||
#include <pspaudio.h>
|
#include <pspaudio.h>
|
||||||
#include <pspthreadman.h>
|
#include <pspthreadman.h>
|
||||||
|
|
||||||
@@ -92,6 +93,9 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
|
|||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
stream->platform.finished = false;
|
stream->platform.finished = false;
|
||||||
|
stream->platform.startFrame = stream->startFrame;
|
||||||
|
stream->startFrame = 0;
|
||||||
|
stream->seeking = false;
|
||||||
stream->platform.playRequested = true;
|
stream->platform.playRequested = true;
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
@@ -125,7 +129,22 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
|||||||
stream->platform.playRequested = false;
|
stream->platform.playRequested = false;
|
||||||
|
|
||||||
const size_t totalFrames = stream->dataSize / frameSize;
|
const size_t totalFrames = stream->dataSize / frameSize;
|
||||||
size_t position = 0;
|
|
||||||
|
// loopEndFrame/loopToFrame define the loop segment [loopToFrame,
|
||||||
|
// loopEndFrame) that a looping pass wraps within, once it's reached -
|
||||||
|
// defaulting to the whole buffer (loopStart == -1, loopTo == 0) so
|
||||||
|
// behaviour is unchanged when no explicit loop points are configured.
|
||||||
|
const size_t loopEndFrame = stream->loopStart >= 0
|
||||||
|
? mathMin((size_t) (stream->loopStart * stream->pcm.sampleRate), totalFrames)
|
||||||
|
: totalFrames;
|
||||||
|
const size_t loopToFrame = mathMin(
|
||||||
|
(size_t) (stream->loopTo * stream->pcm.sampleRate), loopEndFrame
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only the very first pass honors an explicit seek (audioStreamSetPosition,
|
||||||
|
// captured into platform.startFrame by audioStreamPSPBuffer()) - every
|
||||||
|
// subsequent loop wraps to loopToFrame instead.
|
||||||
|
size_t position = stream->platform.startFrame;
|
||||||
bool_t reachedEnd = false;
|
bool_t reachedEnd = false;
|
||||||
|
|
||||||
// Every call always sends a full, constant-size AUDIO_PSP_CHUNK_FRAMES
|
// Every call always sends a full, constant-size AUDIO_PSP_CHUNK_FRAMES
|
||||||
@@ -139,7 +158,14 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
|||||||
// better option (see audiostream.c), but here the thread can just
|
// better option (see audiostream.c), but here the thread can just
|
||||||
// keep going and call onLoop itself instead.
|
// keep going and call onLoop itself instead.
|
||||||
while(!threadShouldStop(thread) && !reachedEnd) {
|
while(!threadShouldStop(thread) && !reachedEnd) {
|
||||||
const size_t framesRemaining = totalFrames - position;
|
// Normally bounded by loopEndFrame (the loop segment's end), but a
|
||||||
|
// seek can legitimately land past it (e.g. into an outro after the
|
||||||
|
// loop point) - in that case play out to the true end of the buffer
|
||||||
|
// once instead of underflowing framesRemaining.
|
||||||
|
const size_t currentEndFrame = position < loopEndFrame
|
||||||
|
? loopEndFrame
|
||||||
|
: totalFrames;
|
||||||
|
const size_t framesRemaining = currentEndFrame - position;
|
||||||
const size_t framesThisChunk = framesRemaining < AUDIO_PSP_CHUNK_FRAMES
|
const size_t framesThisChunk = framesRemaining < AUDIO_PSP_CHUNK_FRAMES
|
||||||
? framesRemaining
|
? framesRemaining
|
||||||
: AUDIO_PSP_CHUNK_FRAMES;
|
: AUDIO_PSP_CHUNK_FRAMES;
|
||||||
@@ -156,17 +182,33 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
|||||||
|
|
||||||
if(reachesEndThisChunk && willLoop) {
|
if(reachesEndThisChunk && willLoop) {
|
||||||
// Instead of padding the rest of this constant-size chunk with
|
// Instead of padding the rest of this constant-size chunk with
|
||||||
// silence, fill it with the start of the next pass - a looping
|
// silence, fill it with the start of the loop segment (loopToFrame)
|
||||||
// stream should never have dead air baked into its output at all.
|
// - a looping stream should never have dead air baked into its
|
||||||
// Not faded (unlike the true-end case below): a wrap only sounds
|
// output at all. Not faded (unlike the true-end case below): a wrap
|
||||||
// seamless if the source data already loops cleanly (true for the
|
// only sounds seamless if the source data already loops cleanly
|
||||||
// shared test tone, deliberately authored at 441Hz for exactly
|
// (true for the shared test tone, deliberately authored at 441Hz
|
||||||
// this), matching the same assumption Dolphin's native hardware
|
// for exactly this), matching the same assumption Dolphin's native
|
||||||
// looping makes - imperfectly-authored loop content will still
|
// hardware looping makes - imperfectly-authored loop content will
|
||||||
// click here.
|
// still click here.
|
||||||
wrapFrames = remainderFrames < totalFrames ? remainderFrames : totalFrames;
|
//
|
||||||
|
// NOTE: if the loop segment (loopEndFrame - loopToFrame) is shorter
|
||||||
|
// than one hardware chunk (AUDIO_PSP_CHUNK_FRAMES, ~23ms @ 44100Hz),
|
||||||
|
// only a single copy of it fills the remainder here rather than
|
||||||
|
// repeating it to fill the whole chunk - any leftover space is
|
||||||
|
// zero-padded (a brief, audible gap) and loopCount still only
|
||||||
|
// increments once per hardware chunk, not once per actual loop
|
||||||
|
// repeat. Not hit by anything in this codebase today (the shared
|
||||||
|
// test tone's default whole-buffer loop is 1 second), but a future
|
||||||
|
// short music-loop tail would need this generalized to a
|
||||||
|
// repeat-fill instead.
|
||||||
|
const size_t loopSegmentFrames = loopEndFrame - loopToFrame;
|
||||||
|
wrapFrames = remainderFrames < loopSegmentFrames
|
||||||
|
? remainderFrames
|
||||||
|
: loopSegmentFrames;
|
||||||
memoryCopy(
|
memoryCopy(
|
||||||
chunk + (framesThisChunk * channels), stream->data, wrapFrames * frameSize
|
chunk + (framesThisChunk * channels),
|
||||||
|
stream->data + (loopToFrame * frameSize),
|
||||||
|
wrapFrames * frameSize
|
||||||
);
|
);
|
||||||
if(wrapFrames < remainderFrames) {
|
if(wrapFrames < remainderFrames) {
|
||||||
memoryZero(
|
memoryZero(
|
||||||
@@ -196,13 +238,12 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
|||||||
|
|
||||||
// Re-read every chunk (~23ms at 44100Hz) so SetVolume/SetDirectionality
|
// Re-read every chunk (~23ms at 44100Hz) so SetVolume/SetDirectionality
|
||||||
// take effect mid-playback, unlike the platform's other one-shot calls.
|
// take effect mid-playback, unlike the platform's other one-shot calls.
|
||||||
float_t pan = (float_t) stream->directionality / 128.0f;
|
float_t leftFactor, rightFactor;
|
||||||
if(pan < -1.0f) pan = -1.0f;
|
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
||||||
if(pan > 1.0f) pan = 1.0f;
|
|
||||||
|
|
||||||
const int baseVolume = (stream->volume * PSP_AUDIO_VOLUME_MAX) / 0xFF;
|
const int baseVolume = (stream->volume * PSP_AUDIO_VOLUME_MAX) / 0xFF;
|
||||||
const int leftVolume = (int) (baseVolume * (pan > 0 ? (1.0f - pan) : 1.0f));
|
const int leftVolume = (int) (baseVolume * leftFactor);
|
||||||
const int rightVolume = (int) (baseVolume * (pan < 0 ? (1.0f + pan) : 1.0f));
|
const int rightVolume = (int) (baseVolume * rightFactor);
|
||||||
|
|
||||||
sceAudioOutputPannedBlocking(
|
sceAudioOutputPannedBlocking(
|
||||||
stream->platform.channel, leftVolume, rightVolume, chunk
|
stream->platform.channel, leftVolume, rightVolume, chunk
|
||||||
@@ -216,7 +257,7 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
|||||||
// starve the next chunk). audioStreamUpdate() picks this up and
|
// starve the next chunk). audioStreamUpdate() picks this up and
|
||||||
// fires onLoop safely from the main thread instead.
|
// fires onLoop safely from the main thread instead.
|
||||||
stream->loopCount++;
|
stream->loopCount++;
|
||||||
position = wrapFrames;
|
position = loopToFrame + wrapFrames;
|
||||||
} else {
|
} else {
|
||||||
reachedEnd = true;
|
reachedEnd = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ typedef struct {
|
|||||||
// a pass; cleared by the thread once it picks it up.
|
// a pass; cleared by the thread once it picks it up.
|
||||||
volatile bool_t playRequested;
|
volatile bool_t playRequested;
|
||||||
|
|
||||||
|
// Frame offset the next pass should start from - captured synchronously
|
||||||
|
// from audiostream_t.startFrame by audioStreamPSPBuffer() (which also
|
||||||
|
// resets that field to 0) rather than read directly by the feeder thread,
|
||||||
|
// since the thread only wakes up asynchronously and audiostream_t's
|
||||||
|
// shared field may already have moved on to a different value by then.
|
||||||
|
size_t startFrame;
|
||||||
|
|
||||||
// Set by the thread once it has fed the last chunk of a pass.
|
// Set by the thread once it has fed the last chunk of a pass.
|
||||||
volatile bool_t finished;
|
volatile bool_t finished;
|
||||||
} audiostreampsp_t;
|
} audiostreampsp_t;
|
||||||
|
|||||||
Reference in New Issue
Block a user