feat: adopt IINA-style playback window
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user