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->loopTo = 0;
|
||||
stream->duration = 0;
|
||||
stream->startFrame = 0;
|
||||
stream->seeking = false;
|
||||
stream->user = NULL;
|
||||
stream->onLoop = NULL;
|
||||
stream->onEnd = NULL;
|
||||
@@ -44,15 +46,52 @@ void audioStreamPause(audiostream_t *stream) {
|
||||
void audioStreamSetPosition(audiostream_t *stream, const float_t position) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
|
||||
// Clamp time.
|
||||
// Wrap into [0, duration).
|
||||
float_t t = position;
|
||||
while(t < 0) 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
|
||||
// next update() call.
|
||||
// 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?
|
||||
stream->startFrame = (size_t) (t * stream->pcm.sampleRate);
|
||||
stream->seeking = true;
|
||||
|
||||
// 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) {
|
||||
@@ -103,10 +142,7 @@ errorret_t audioStreamUpdate(audiostream_t *stream) {
|
||||
|
||||
// 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
|
||||
// consumes it. Looping currently only supports restarting from the very
|
||||
// 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.
|
||||
// consumes it.
|
||||
// Checked in this order (finished-check before needs-buffering) so that a
|
||||
// loop restart falls straight through into re-buffering within this same
|
||||
// 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) {
|
||||
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) {
|
||||
stream->onLoop(stream);
|
||||
}
|
||||
|
||||
@@ -63,15 +63,40 @@ typedef struct audiostream_s {
|
||||
// TODO: Can we use a 3D vector + Pro Logic II?
|
||||
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;
|
||||
|
||||
// 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;
|
||||
|
||||
// Cached duration of the stream in seconds.
|
||||
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
|
||||
void *user;
|
||||
void (*onLoop)(audiostream_t *stream);
|
||||
@@ -128,12 +153,50 @@ void audioStreamPlay(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);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
||||
@@ -42,10 +42,8 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
|
||||
|
||||
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 pan = (float_t) stream->directionality / 128.0f;
|
||||
if(pan < -1.0f) pan = -1.0f;
|
||||
if(pan > 1.0f) pan = 1.0f;
|
||||
float_t leftFactor, rightFactor;
|
||||
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
||||
|
||||
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.channels = stream->pcm.channels;
|
||||
config.pitch = 1.0f;
|
||||
config.left_volume = baseVolume * (pan > 0 ? (1.0f - pan) : 1.0f);
|
||||
config.right_volume = baseVolume * (pan < 0 ? (1.0f + pan) : 1.0f);
|
||||
config.left_volume = baseVolume * leftFactor;
|
||||
config.right_volume = baseVolume * rightFactor;
|
||||
// The DSP DMAs frame_data_ptr directly out of main memory, bypassing the
|
||||
// CPU cache, and rejects any pointer in cached virtual address space
|
||||
// (0x8xxxxxxx.. - checked as "negative" internally, returned as
|
||||
@@ -68,28 +66,31 @@ errorret_t audioStreamDolphinBuffer(audiostream_t *stream) {
|
||||
DCFlushRange(stream->data, stream->dataSize);
|
||||
config.frame_data_ptr = MEM_VIRTUAL_TO_PHYSICAL(stream->data);
|
||||
config.frame_count = (u32) (stream->dataSize / frameSize);
|
||||
config.start_offset = (u32) stream->startFrame;
|
||||
stream->startFrame = 0;
|
||||
stream->seeking = false;
|
||||
config.voice_callback = audioStreamDolphinVoiceCallback;
|
||||
config.stream_callback = NULL; // single-buffer playback
|
||||
config.user_pointer = stream;
|
||||
|
||||
// Loop entirely in hardware rather than mirroring PSP/Linux's
|
||||
// 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
|
||||
// to create a gap in the first place (unlike a software restart, which
|
||||
// always costs at least a little). Only restart-from-the-very-start is
|
||||
// supported elsewhere in this codebase (loopTo isn't honored yet), which
|
||||
// matches exactly what loop_start_offset=0 gives here.
|
||||
// wraps loop_end_offset back to loop_start_offset on its own, so there's
|
||||
// no restart latency to create a gap in the first place (unlike a
|
||||
// software restart, which always costs at least a little).
|
||||
//
|
||||
// Trade-off: onLoop never fires for a Dolphin voice looping this way -
|
||||
// there's no ANSND_VOICE_STATE for "wrapped", only state transitions like
|
||||
// FINISHED/STOPPED, which a looping voice never reaches. And unlike
|
||||
// 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
|
||||
// cleanly into its first (true today only because the shared 441Hz test
|
||||
// tone was deliberately chosen to divide evenly into the sample rate).
|
||||
// point - it requires loopStart's frame to already flow cleanly into
|
||||
// loopTo's (true today only because the shared 441Hz test tone was
|
||||
// deliberately chosen to divide evenly into the sample rate).
|
||||
if(stream->state & AUDIO_STREAM_STATE_LOOPING) {
|
||||
config.loop_start_offset = 0;
|
||||
config.loop_end_offset = config.frame_count - 1;
|
||||
config.loop_start_offset = (u32) (stream->loopTo * stream->pcm.sampleRate);
|
||||
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);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "audio/audiostream.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/math.h"
|
||||
|
||||
// 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
|
||||
@@ -44,17 +45,56 @@ errorret_t audioStreamLinuxDispose(audiostream_t *stream) {
|
||||
errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
|
||||
uint8_t *mixed = memoryAllocate(stream->dataSize);
|
||||
memoryZero(mixed, stream->dataSize);
|
||||
const size_t frameSize = stream->pcm.channels * sizeof(int16_t);
|
||||
const size_t totalFrames = stream->dataSize / frameSize;
|
||||
|
||||
// Consumed synchronously (right here, in the same call that decided to
|
||||
// (re)buffer) rather than left for later - see startFrame's own comment.
|
||||
const size_t startFrame = mathMin(stream->startFrame, totalFrames);
|
||||
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, stream->data, AUDIO_S16SYS, (Uint32) stream->dataSize,
|
||||
mixed, segment, AUDIO_S16SYS, (Uint32) bytes,
|
||||
(stream->volume * SDL_MIX_MAXVOLUME) / 0xFF
|
||||
);
|
||||
|
||||
int queued = SDL_QueueAudio(
|
||||
stream->platform.device, mixed, (Uint32) stream->dataSize
|
||||
);
|
||||
queued = SDL_QueueAudio(stream->platform.device, mixed, (Uint32) bytes);
|
||||
memoryFree(mixed);
|
||||
}
|
||||
|
||||
if(queued != 0) {
|
||||
errorThrow("Failed to queue SDL2 audio data: %s", SDL_GetError());
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "audio/audiostream.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/math.h"
|
||||
#include <pspaudio.h>
|
||||
#include <pspthreadman.h>
|
||||
|
||||
@@ -92,6 +93,9 @@ errorret_t audioStreamPSPBuffer(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
|
||||
stream->platform.finished = false;
|
||||
stream->platform.startFrame = stream->startFrame;
|
||||
stream->startFrame = 0;
|
||||
stream->seeking = false;
|
||||
stream->platform.playRequested = true;
|
||||
|
||||
errorOk();
|
||||
@@ -125,7 +129,22 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
||||
stream->platform.playRequested = false;
|
||||
|
||||
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;
|
||||
|
||||
// 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
|
||||
// keep going and call onLoop itself instead.
|
||||
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
|
||||
? framesRemaining
|
||||
: AUDIO_PSP_CHUNK_FRAMES;
|
||||
@@ -156,17 +182,33 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
||||
|
||||
if(reachesEndThisChunk && willLoop) {
|
||||
// Instead of padding the rest of this constant-size chunk with
|
||||
// silence, fill it with the start of the next pass - a looping
|
||||
// stream should never have dead air baked into its output at all.
|
||||
// Not faded (unlike the true-end case below): a wrap only sounds
|
||||
// seamless if the source data already loops cleanly (true for the
|
||||
// shared test tone, deliberately authored at 441Hz for exactly
|
||||
// this), matching the same assumption Dolphin's native hardware
|
||||
// looping makes - imperfectly-authored loop content will still
|
||||
// click here.
|
||||
wrapFrames = remainderFrames < totalFrames ? remainderFrames : totalFrames;
|
||||
// silence, fill it with the start of the loop segment (loopToFrame)
|
||||
// - a looping stream should never have dead air baked into its
|
||||
// output at all. Not faded (unlike the true-end case below): a wrap
|
||||
// only sounds seamless if the source data already loops cleanly
|
||||
// (true for the shared test tone, deliberately authored at 441Hz
|
||||
// for exactly this), matching the same assumption Dolphin's native
|
||||
// hardware looping makes - imperfectly-authored loop content will
|
||||
// still click here.
|
||||
//
|
||||
// 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(
|
||||
chunk + (framesThisChunk * channels), stream->data, wrapFrames * frameSize
|
||||
chunk + (framesThisChunk * channels),
|
||||
stream->data + (loopToFrame * frameSize),
|
||||
wrapFrames * frameSize
|
||||
);
|
||||
if(wrapFrames < remainderFrames) {
|
||||
memoryZero(
|
||||
@@ -196,13 +238,12 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
||||
|
||||
// Re-read every chunk (~23ms at 44100Hz) so SetVolume/SetDirectionality
|
||||
// take effect mid-playback, unlike the platform's other one-shot calls.
|
||||
float_t pan = (float_t) stream->directionality / 128.0f;
|
||||
if(pan < -1.0f) pan = -1.0f;
|
||||
if(pan > 1.0f) pan = 1.0f;
|
||||
float_t leftFactor, rightFactor;
|
||||
audioStreamGetPanFactors(stream, &leftFactor, &rightFactor);
|
||||
|
||||
const int baseVolume = (stream->volume * PSP_AUDIO_VOLUME_MAX) / 0xFF;
|
||||
const int leftVolume = (int) (baseVolume * (pan > 0 ? (1.0f - pan) : 1.0f));
|
||||
const int rightVolume = (int) (baseVolume * (pan < 0 ? (1.0f + pan) : 1.0f));
|
||||
const int leftVolume = (int) (baseVolume * leftFactor);
|
||||
const int rightVolume = (int) (baseVolume * rightFactor);
|
||||
|
||||
sceAudioOutputPannedBlocking(
|
||||
stream->platform.channel, leftVolume, rightVolume, chunk
|
||||
@@ -216,7 +257,7 @@ void audioStreamPSPThreadFeed(thread_t *thread) {
|
||||
// starve the next chunk). audioStreamUpdate() picks this up and
|
||||
// fires onLoop safely from the main thread instead.
|
||||
stream->loopCount++;
|
||||
position = wrapFrames;
|
||||
position = loopToFrame + wrapFrames;
|
||||
} else {
|
||||
reachedEnd = true;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,13 @@ typedef struct {
|
||||
// a pass; cleared by the thread once it picks it up.
|
||||
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.
|
||||
volatile bool_t finished;
|
||||
} audiostreampsp_t;
|
||||
|
||||
Reference in New Issue
Block a user