feat: adopt IINA-style playback window

This commit is contained in:
2026-10-10 01:35:49 +02:00
parent df2456f559
commit 462cb52eb2
8 changed files with 654 additions and 76 deletions
+82 -7
View File
@@ -18,11 +18,18 @@ size_t capturedFrames = 0;
void require(bool condition, const char *message) {
if (!condition) { std::fprintf(stderr, "FAIL audio: %s\n", message); std::exit(1); }
}
void play(const std::string &path) {
void play(const std::string &path, float volume = 1, bool changeWhilePlaying = false) {
capturedFrames = 0;
std::unique_ptr<Player, decltype(&player_destroy)> player(player_create(), player_destroy);
require(player && player_open(player.get(), path.c_str()), "open fixture");
bool started = false, ended = false;
require(bool(player), "create fixture player");
require(player_set_volume(player.get(), volume), "set fixture volume");
for (float invalid : {NAN, INFINITY, -INFINITY, -.01f, 1.01f}) {
require(!player_set_volume(player.get(), invalid), "invalid volume rejected");
require(player_get_volume(player.get()) == volume, "invalid volume leaves target unchanged");
}
require(player_open(player.get(), path.c_str()), "open fixture");
bool started = false, ended = false, sought = false, changed = false;
uint64_t seekRevision = 0;
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(12);
while (std::chrono::steady_clock::now() < deadline) {
auto state = player_get_state(player.get());
@@ -31,18 +38,45 @@ void play(const std::string &path) {
require(false, "playback error");
}
if (state.status == PLAYER_PAUSED && !started) {
require(player_get_volume(player.get()) == volume, "volume survives open and seek");
if (volume != 1 && !sought) {
seekRevision = state.revision;
require(player_seek(player.get(), 0), "seek with gain configured");
sought = true;
continue;
}
if (sought && state.revision == seekRevision) {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
continue;
}
require(player_play(player.get()), "play fixture");
started = true;
}
if (state.status == PLAYER_ENDED) { ended = true; break; }
if (changeWhilePlaying && !changed && state.status == PLAYER_PLAYING && state.position > .3) {
require(player_set_volume(player.get(), .25f), "change volume during playback");
changed = true;
}
if (state.status == PLAYER_ENDED) {
require(state.position >= state.duration - .01, "gain does not shorten the media timeline");
ended = true;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
require(ended, "playback completes");
require(!changeWhilePlaying || changed, "live volume change was exercised");
const float finalVolume = changeWhilePlaying ? .25f : volume;
require(player_get_volume(player.get()) == finalVolume, "volume survives playback");
require(player_close(player.get()), "close fixture");
while (std::chrono::steady_clock::now() < deadline && player_get_state(player.get()).status != PLAYER_EMPTY)
std::this_thread::sleep_for(std::chrono::milliseconds(2));
require(player_get_state(player.get()).status == PLAYER_EMPTY, "close completes");
require(player_get_volume(player.get()) == finalVolume, "volume survives close");
player.reset(); // Stop/join before inspecting callback-owned capture storage.
}
size_t firstActive(const float *samples, size_t frames) {
size_t firstActive(const float *samples, size_t frames, float threshold = .002f) {
size_t first = 0;
while (first < frames && std::abs(samples[first * 2]) < .002f) ++first;
while (first < frames && std::abs(samples[first * 2]) < threshold) ++first;
require(first < frames, "tone is present");
return first;
}
@@ -57,6 +91,7 @@ void player_check_audio(const float *samples, size_t frames) {
int main(int argc, char **argv) {
require(argc == 2, "fixture directory required");
require(!player_set_volume(nullptr, .5f) && player_get_volume(nullptr) == 0, "null volume handle");
std::string directory = argv[1];
for (const char *name : {"tone441.wav", "tone480.wav", "tone441.mka", "tone-aac.m4a"}) {
std::string path = directory + "/" + name;
@@ -79,6 +114,46 @@ int main(int argc, char **argv) {
maximumError = std::max(maximumError, std::abs(capture[offset * 2 + i] - reference[i]));
std::printf("%s: max sample error %.8f\n", name, maximumError);
require(maximumError < .00003f, "continuous callback PCM matches reference (no inserted/dropped samples)");
if (std::string(name) == "tone480.wav") {
play(path, .25f);
size_t quietStart = firstActive(capture.data(), capturedFrames, .002f * .25f);
require(quietStart >= referenceStart, "fractional-volume alignment");
size_t quietOffset = quietStart - referenceStart;
require(capturedFrames >= quietOffset + frames, "fractional volume consumes all media");
float gainError = 0;
for (size_t sample = 0; sample < reference.size(); ++sample)
gainError = std::max(gainError, std::abs(capture[quietOffset * 2 + sample] - reference[sample] * .25f));
require(gainError < .00003f, "volume set while stopped applies from the first sample");
play(path, 0);
require(capturedFrames >= frames, "mute consumes the entire media timeline");
for (size_t sample = 0; sample < capturedFrames * 2; ++sample)
require(capture[sample] == 0, "muted startup never leaks audible samples");
std::printf("volume: max quarter-gain error %.8f; mute silent from startup\n", gainError);
play(path, 1, true);
size_t liveOffset = firstActive(capture.data(), capturedFrames) - referenceStart;
require(capturedFrames >= liveOffset + frames, "live volume change preserves all media samples");
size_t firstChange = frames, settled = frames, previousFrame = 0;
float previousGain = 1;
for (size_t frame = 1; frame < frames; ++frame) {
float expected = reference[frame * 2];
if (std::abs(expected) < .003f) continue; // Gain is undefined at zero crossings.
float gain = capture[(liveOffset + frame) * 2] / expected;
require(gain >= .2499f && gain <= 1.0001f, "live gain never overshoots");
require(std::abs(gain - previousGain) / (frame - previousFrame) < .01f,
"live gain changes smoothly rather than popping");
if (firstChange == frames && gain < .9999f) firstChange = frame;
if (firstChange != frames && settled == frames && std::abs(gain - .25f) < .0001f)
settled = frame;
previousGain = gain;
previousFrame = frame;
}
require(firstChange > 12000 && settled < frames && settled - firstChange <= 256,
"live gain reaches its target within approximately five milliseconds");
for (size_t sample = (settled + 1) * 2; sample < reference.size(); ++sample)
require(std::abs(capture[liveOffset * 2 + sample] - reference[sample] * .25f) < .00003f,
"live gain settles without corrupting subsequent PCM");
}
}
// Continuity must not erase a real 250 ms PTS gap in the source.
play(directory + "/tone-gap.mka");
@@ -92,5 +167,5 @@ int main(int argc, char **argv) {
energy += value * value;
}
require(energy / 4800 > .001, "audible tone resumes after real gap");
std::puts("PASS callback PCM continuity at 44.1/48 kHz, coarse timestamps, and real timestamp gap");
std::puts("PASS callback PCM continuity, 5 ms volume ramp, fractional gain, mute, and unchanged timeline");
}