mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-14 10:09:32 +01:00
Headless: Let --audio-sink drive the granular mixer
--audio-granular picks the granular mixer (the Audio sync mode setting), and the sink then reports the queue's level and cap, and its underruns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b74fad44f
commit
d67d47c4a3
4 files changed
+79
-10
No files matched your search
@@ -5,6 +5,7 @@
|
||||
#include <cstring>
|
||||
|
||||
#include "Core/Config.h"
|
||||
#include "Core/ConfigValues.h"
|
||||
#include "Core/CmdLine.h"
|
||||
#include "Core/WebServer.h"
|
||||
#include "Core/Util/PathUtil.h"
|
||||
@@ -268,6 +269,7 @@ static const CommandLineParam g_autoParams[] = {
|
||||
{POFF(lagSync), CmdParamType::Bool, "lag-sync", '\0', "Keep emulated time in step with the wall clock every millisecond, not every frame (the Force real clock sync setting)"},
|
||||
{POFF(frameStats), CmdParamType::Bool, "frame-stats", '\0', "Print flips per emulated second at the end of the run, counted from --state's load", CmdLineMode::Headless},
|
||||
{POFF(audioSinkPeriod), CmdParamType::Int, "audio-sink", '\0', "Pull audio like an output device, FRAMES at 48 kHz at a time, and print the buffer's stats at the end (with --realtime)", CmdLineMode::Headless},
|
||||
{POFF(audioGranular), CmdParamType::Bool, "audio-granular", '\0', "Mix audio with the granular mixer instead of the classic resampler (the Audio sync mode setting)"},
|
||||
{POFF(maxScreenshotError), CmdParamType::Double, "max-mse", '\0', "Maximum allowed MSE error for screenshot comparison", CmdLineMode::Headless},
|
||||
{POFF(mountIso), CmdParamType::String, "mount", 'm', "Mount ISO/CSO on umd1: when booting an ELF/PBP or --vsh"},
|
||||
{POFF(unpackUpdater), CmdParamType::String, "unpack-updater", '\0', "Unpack the firmware in an updater EBOOT.PBP into DIR and exit", CmdLineMode::Headless},
|
||||
@@ -614,6 +616,10 @@ void CommandLineOptions::ApplyToConfig() const {
|
||||
g_Config.bForceLagSync = lagSync.value();
|
||||
g_Config.DoNotSaveSetting(&g_Config.bForceLagSync);
|
||||
}
|
||||
if (audioGranular.has_value()) {
|
||||
g_Config.iAudioPlaybackMode = (int)(audioGranular.value() ? AudioSyncMode::GRANULAR : AudioSyncMode::CLASSIC_PITCH);
|
||||
g_Config.DoNotSaveSetting(&g_Config.iAudioPlaybackMode);
|
||||
}
|
||||
if (fuseId.has_value()) {
|
||||
g_Config.sFuseID = fuseId.value();
|
||||
g_Config.DoNotSaveSetting(&g_Config.sFuseID);
|
||||
|
||||
@@ -197,6 +197,7 @@ struct CommandLineOptions {
|
||||
// Headless: pull audio through the resampler like an output device with this period, in frames
|
||||
// at 48 kHz, and print how full the buffer was and how often it ran dry.
|
||||
std::optional<int> audioSinkPeriod;
|
||||
std::optional<bool> audioGranular;
|
||||
// Headless: which of several instances running at once this is (sets PPSSPP_ID, and with it the
|
||||
// instance's 127.0.0.N address and MAC), and whether to run at PSP speed rather than flat out.
|
||||
// Both are for ad hoc tests where instances talk to each other.
|
||||
|
||||
@@ -50,6 +50,12 @@ public:
|
||||
void GetAudioDebugStats(char *buf, size_t bufSize);
|
||||
void ResetStatCounters();
|
||||
|
||||
// In stereo samples at the input rate (each queued granule adds GRANULE_OVERLAP).
|
||||
int QueuedSamples() const { return (int)(m_queue_head.load() - m_queue_tail.load()) * (int)GRANULE_OVERLAP; }
|
||||
int QueueCapSamples() const { return (int)m_granule_queue_size.load() * (int)GRANULE_OVERLAP; }
|
||||
int Underruns() const { return underruns_; }
|
||||
int Overruns() const { return overruns_; }
|
||||
|
||||
static constexpr u32 GRANULE_SIZE = 256;
|
||||
|
||||
private:
|
||||
|
||||
+66
-10
@@ -69,6 +69,8 @@
|
||||
#include "Core/HLE/__sceAudio.h"
|
||||
#include "Core/HLE/sceCtrl.h"
|
||||
#include "Core/HLE/sceDisplay.h"
|
||||
#include "Core/HW/Display.h"
|
||||
#include "Core/HW/GranularMixer.h"
|
||||
#include "Core/HW/StereoResampler.h"
|
||||
#include "Core/MIPS/MIPSTables.h"
|
||||
#include "Core/System.h"
|
||||
@@ -157,9 +159,13 @@ class AudioSink {
|
||||
public:
|
||||
static constexpr int SAMPLE_RATE = 48000;
|
||||
|
||||
void Start(int periodFrames) {
|
||||
void Start(int periodFrames, bool granular) {
|
||||
periodFrames_ = periodFrames;
|
||||
resampler_ = std::make_unique<StereoResampler>();
|
||||
if (granular) {
|
||||
granular_ = std::make_unique<GranularMixer>();
|
||||
} else {
|
||||
resampler_ = std::make_unique<StereoResampler>();
|
||||
}
|
||||
thread_ = std::thread([this] { Run(); });
|
||||
}
|
||||
void Stop() {
|
||||
@@ -172,13 +178,31 @@ public:
|
||||
int PeriodFrames() const { return periodFrames_; }
|
||||
// Counting starts once the game runs in real time, not while a state loads.
|
||||
void StartCounting() { counting_ = true; }
|
||||
void Push(const s32 *audio, int numSamples, float volume) { resampler_->PushSamples(audio, numSamples, volume); }
|
||||
void Clear() { resampler_->Clear(); }
|
||||
void Push(const s32 *audio, int numSamples, float volume) {
|
||||
if (granular_) {
|
||||
granular_->PushSamples(audio, numSamples, volume);
|
||||
} else {
|
||||
resampler_->PushSamples(audio, numSamples, volume);
|
||||
}
|
||||
}
|
||||
// Like the app, which has nothing to clear in the granular mixer.
|
||||
void Clear() {
|
||||
if (resampler_) {
|
||||
resampler_->Clear();
|
||||
}
|
||||
}
|
||||
|
||||
void Print() const {
|
||||
const double toMs = 1000.0 / 44100.0;
|
||||
const double avg = pulls_ ? (double)sumBuffered_ / pulls_ * toMs : 0.0;
|
||||
if (granular_) {
|
||||
fprintf(stderr, "Audio sink (granular): %d pulls of %d frames, %d with an underrun (%d underruns), queue before a pull %.1f/%.1f/%.1f ms min/avg/max, cap %.1f/%.1f/%.1f ms min/avg/max, %d overruns\n",
|
||||
pulls_, periodFrames_, shortPulls_, underruns_, minBuffered_ * toMs, avg, maxBuffered_ * toMs,
|
||||
minCap_ * toMs, pulls_ ? (double)sumCap_ / pulls_ * toMs : 0.0, maxCap_ * toMs, overruns_);
|
||||
return;
|
||||
}
|
||||
fprintf(stderr, "Audio sink: %d pulls of %d frames, %d short (%d frames missing), buffer before a pull %.1f/%.1f/%.1f ms min/avg/max, target %.1f ms, %.1f ms of excess dropped\n",
|
||||
pulls_, periodFrames_, shortPulls_, missingFrames_, minBuffered_ * toMs, pulls_ ? (double)sumBuffered_ / pulls_ * toMs : 0.0,
|
||||
pulls_, periodFrames_, shortPulls_, missingFrames_, minBuffered_ * toMs, avg,
|
||||
maxBuffered_ * toMs, resampler_->CurrentTargetBufferSize() * toMs, resampler_->DroppedExcess() * toMs);
|
||||
}
|
||||
|
||||
@@ -196,15 +220,40 @@ private:
|
||||
// Stalled (the debugger, say), start over rather than pulling a backlog.
|
||||
next = now;
|
||||
}
|
||||
const int buffered = resampler_->BufferedSamples();
|
||||
const int mixed = resampler_->Mix(out.data(), periodFrames_, false, SAMPLE_RATE);
|
||||
int buffered;
|
||||
bool isShort;
|
||||
if (granular_) {
|
||||
buffered = granular_->QueuedSamples();
|
||||
const int underrunsBefore = granular_->Underruns();
|
||||
const int overrunsBefore = granular_->Overruns();
|
||||
// As the app does, see NativeMix.
|
||||
float vps, fpsEstimate, actualFps;
|
||||
__DisplayGetFPS(&vps, &fpsEstimate, &actualFps);
|
||||
granular_->Mix(out.data(), periodFrames_, SAMPLE_RATE, fpsEstimate);
|
||||
const int newUnderruns = granular_->Underruns() - underrunsBefore;
|
||||
isShort = newUnderruns > 0;
|
||||
if (counting_) {
|
||||
underruns_ += newUnderruns;
|
||||
overruns_ += granular_->Overruns() - overrunsBefore;
|
||||
const int cap = granular_->QueueCapSamples();
|
||||
sumCap_ += cap;
|
||||
minCap_ = std::min(minCap_, cap);
|
||||
maxCap_ = std::max(maxCap_, cap);
|
||||
}
|
||||
} else {
|
||||
buffered = resampler_->BufferedSamples();
|
||||
const int mixed = resampler_->Mix(out.data(), periodFrames_, false, SAMPLE_RATE);
|
||||
isShort = mixed < periodFrames_;
|
||||
if (counting_ && isShort) {
|
||||
missingFrames_ += periodFrames_ - mixed;
|
||||
}
|
||||
}
|
||||
if (!counting_) {
|
||||
continue;
|
||||
}
|
||||
pulls_++;
|
||||
if (mixed < periodFrames_) {
|
||||
if (isShort) {
|
||||
shortPulls_++;
|
||||
missingFrames_ += periodFrames_ - mixed;
|
||||
}
|
||||
sumBuffered_ += buffered;
|
||||
minBuffered_ = std::min(minBuffered_, buffered);
|
||||
@@ -213,6 +262,7 @@ private:
|
||||
}
|
||||
|
||||
std::unique_ptr<StereoResampler> resampler_;
|
||||
std::unique_ptr<GranularMixer> granular_;
|
||||
std::thread thread_;
|
||||
std::atomic<bool> stop_{};
|
||||
std::atomic<bool> counting_{};
|
||||
@@ -223,6 +273,12 @@ private:
|
||||
int64_t sumBuffered_ = 0;
|
||||
int minBuffered_ = INT_MAX;
|
||||
int maxBuffered_ = 0;
|
||||
// Granular only.
|
||||
int underruns_ = 0;
|
||||
int overruns_ = 0;
|
||||
int64_t sumCap_ = 0;
|
||||
int minCap_ = INT_MAX;
|
||||
int maxCap_ = 0;
|
||||
};
|
||||
|
||||
static AudioSink g_audioSink;
|
||||
@@ -1645,7 +1701,7 @@ int main(int argc, const char* argv[]) {
|
||||
if (cmdLineOptions.audioSinkPeriod.has_value() && cmdLineOptions.audioSinkPeriod.value() > 0) {
|
||||
// Sound on also changes how the frame throttle waits (DoFrameIdleTiming), as in the app.
|
||||
g_Config.bEnableSound = true;
|
||||
g_audioSink.Start(cmdLineOptions.audioSinkPeriod.value());
|
||||
g_audioSink.Start(cmdLineOptions.audioSinkPeriod.value(), g_Config.iAudioPlaybackMode == (int)AudioSyncMode::GRANULAR);
|
||||
}
|
||||
if (cmdLineOptions.replayEnd.has_value()) {
|
||||
GPURecord::SetReplayDrawLimit(cmdLineOptions.replayEnd.value());
|
||||
|
||||
Reference in new issue
Block a user