212 lines
12 KiB
C++
212 lines
12 KiB
C++
#include "../Sources/Core/player.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
std::array<float, 48000 * 8 * 2> capture{};
|
|
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, float volume = 1, bool changeWhilePlaying = false) {
|
|
capturedFrames = 0;
|
|
std::unique_ptr<Player, decltype(&player_destroy)> player(player_create(), player_destroy);
|
|
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());
|
|
if (state.status == PLAYER_ERROR) {
|
|
std::fprintf(stderr, "%s\n", state.error);
|
|
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 (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, float threshold = .002f) {
|
|
size_t first = 0;
|
|
while (first < frames && std::abs(samples[first * 2]) < threshold) ++first;
|
|
require(first < frames, "tone is present");
|
|
return first;
|
|
}
|
|
void playStepped(const std::string &path, const std::vector<float> &reference) {
|
|
capturedFrames = 0;
|
|
std::unique_ptr<Player, decltype(&player_destroy)> player(player_create(), player_destroy);
|
|
require(bool(player), "create stepped audio player");
|
|
auto wait = [&](auto ready) {
|
|
const 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());
|
|
require(state.status != PLAYER_ERROR, state.error);
|
|
if (ready(state)) return state;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
}
|
|
require(false, "stepped audio operation completes");
|
|
return PlayerState{};
|
|
};
|
|
require(player_open(player.get(), path.c_str()), "open video with PCM audio");
|
|
wait([](auto s) { return s.status == PLAYER_PAUSED; });
|
|
require(player_step_frame(player.get(), -1), "establish first frame");
|
|
wait([](auto s) { return !s.seek_pending; });
|
|
for (int i = 0; i < 32; ++i) require(player_step_frame(player.get(), 1), "queue forward audio frame steps");
|
|
for (int i = 0; i < 19; ++i) require(player_step_frame(player.get(), -1), "queue backward audio frame steps");
|
|
auto selected = wait([](auto s) { return !s.seek_pending; });
|
|
require(std::abs(selected.position - 13.0 / 24) < .0001, "resume starts at selected video frame");
|
|
require(capturedFrames == 0, "frame stepping never starts audio output");
|
|
require(player_play(player.get()), "resume audio after repeated steps");
|
|
auto ended = wait([](auto s) { return s.status == PLAYER_ENDED; });
|
|
require(std::abs(ended.position - 2) < .0001, "stepped audio reaches original media end");
|
|
player.reset(); // Stop/join before inspecting callback-owned storage.
|
|
constexpr size_t sourceStart = 26000; // 13 / 24 seconds at 48 kHz.
|
|
const size_t remaining = reference.size() / 2 - sourceStart;
|
|
const size_t offset = firstActive(capture.data(), capturedFrames);
|
|
require(capturedFrames >= offset + remaining, "all remaining PCM reaches the callback");
|
|
float maximumError = 0;
|
|
for (size_t i = 0; i < remaining * 2; ++i)
|
|
maximumError = std::max(maximumError,
|
|
std::abs(capture[offset * 2 + i] - reference[sourceStart * 2 + i]));
|
|
std::printf("stepped resume: max remaining PCM error %.8f\n", maximumError);
|
|
require(maximumError < .00003f, "resume audio matches selected-frame suffix, without stale or dropped samples");
|
|
}
|
|
}
|
|
|
|
// Linked only into the check build. No allocation, locks, or file I/O in callback.
|
|
void player_check_audio(const float *samples, size_t frames) {
|
|
size_t count = std::min(frames, capture.size() / 2 - capturedFrames);
|
|
std::memcpy(capture.data() + capturedFrames * 2, samples, count * 2 * sizeof(float));
|
|
capturedFrames += count;
|
|
}
|
|
|
|
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;
|
|
std::ifstream input(path + ".f32", std::ios::binary | std::ios::ate);
|
|
require(bool(input), "PCM reference exists");
|
|
auto bytes = input.tellg();
|
|
require(bytes > 0 && bytes % (2 * sizeof(float)) == 0, "stereo reference format");
|
|
std::vector<float> reference(static_cast<size_t>(bytes) / sizeof(float));
|
|
input.seekg(0);
|
|
require(bool(input.read(reinterpret_cast<char *>(reference.data()), bytes)), "read PCM reference");
|
|
play(path);
|
|
size_t frames = reference.size() / 2;
|
|
size_t start = firstActive(capture.data(), capturedFrames);
|
|
size_t referenceStart = firstActive(reference.data(), frames);
|
|
require(start >= referenceStart, "capture alignment");
|
|
size_t offset = start - referenceStart;
|
|
require(capturedFrames >= offset + frames, "all output samples reach callback");
|
|
float maximumError = 0;
|
|
for (size_t i = 0; i < reference.size(); ++i)
|
|
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") {
|
|
playStepped(directory + "/tone-step.mov", reference);
|
|
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");
|
|
size_t offset = firstActive(capture.data(), capturedFrames) - 1;
|
|
require(capturedFrames > offset + 48000 * 2, "gapped media is presented");
|
|
for (size_t i = 51840; i < 57600; ++i)
|
|
require(std::abs(capture[(offset + i) * 2]) < 1e-6f, "real timestamp gap remains silent");
|
|
double energy = 0;
|
|
for (size_t i = 62000; i < 66800; ++i) {
|
|
double value = capture[(offset + i) * 2];
|
|
energy += value * value;
|
|
}
|
|
require(energy / 4800 > .001, "audible tone resumes after real gap");
|
|
std::puts("PASS callback PCM continuity, 5 ms volume ramp, fractional gain, mute, and unchanged timeline");
|
|
}
|