// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 #pragma once #include "core_queue_sink.h" #include #include namespace queue_audio { struct ReadAheadVerdict { bool ok = true; size_t pairs = 0, incomplete = 0; double mismatchedSeconds = 0; std::string reasons; double worst = -1; std::string line() const { return ok ? "LMS read-ahead OK" : "LMS read-ahead INSUFFICIENT"; } }; inline ReadAheadVerdict writeReadAheadReport(const std::vector &rows, std::ostream &report, std::ostream &tsv, double speed = 1, double wallSeconds = -1) { auto cell = [](const std::string &text) { std::string out; for (char c : text) { if (c == '\t') out += "\\t"; else if (c == '\n') out += "\\n"; else if (c == '\r') out += "\\r"; else if (c == '|') out += "\\|"; else if (c == '\\') out += "\\\\"; else out += c; } return out; }; auto count = [](std::optional n) { return n ? std::to_string(*n) : "unknown"; }; std::ostringstream table; table << "occurrence\trate\texact_frames\tstrm\tdecode_complete\tSTMd\tnext_" "strm\taudible_" "start\tSTMs\taudible_end\tsuccessor_lead_s\ttransition\toverlap_" "frames\tpromised_" "frames\tactual_frames\tprefix_" "frames\tpublication\tfault\tcodec\tstartup_delay_s" "\tpair_status\texpected_lms_index\tlms_poll_stamp\tlms_playlist_" "index\tlms_time\tlms_title" "\tlms_matches_audible\tmismatched_seconds\tspeed\tstrm_wall\taudible_start_wall\taudible_end_" "wall\tdecode_complete_wall\tSTMd_wall\tnext_strm_wall\tSTMs_wall\tsuccessor_lead_wall_" "s\tstartup_delay_wall_s\tlms_audible_elapsed_s\tlms_time_error_wall_s\n"; ReadAheadVerdict v; size_t polls = 0, known = 0; for (size_t i = 0; i < rows.size(); ++i) { const auto &r = rows[i]; double next = -1; if (r.source.occurrence < UINT64_MAX / 2) for (size_t j = i + 1; j < rows.size(); ++j) if (rows[j].source.occurrence < UINT64_MAX / 2) { next = rows[j].strm; break; } double end = r.end; bool pendingTail = false; for (const auto &tail : rows) if (tail.source.occurrence == UINT64_MAX - r.source.occurrence) { pendingTail = pendingTail || tail.end < 0; if (tail.end >= 0) end = std::max(end, tail.end); } if (i + 1 < rows.size() && rows[i + 1].prefixFrames) { pendingTail = pendingTail || rows[i + 1].audible < 0; if (rows[i + 1].audible >= 0) end = std::max(end, rows[i + 1].audible); } if (pendingTail) end = -1; const double lead = end >= 0 && next >= 0 ? end - next : -1; std::string status; if (r.source.occurrence >= UINT64_MAX / 2) { status = "continuation"; } else if (end >= 0 && next >= 0) { status = "completed"; if (!v.pairs || lead < v.worst) v.worst = lead; ++v.pairs; if (lead < 12) { v.ok = false; v.reasons += " lead-below-12s"; } } else { ++v.incomplete; status = !r.incomplete.empty() ? r.incomplete : !r.fault.empty() ? "source-fault" : "run-end"; } if (r.storageFault) { v.ok = false; v.reasons += " storage-fault"; } if (r.lmsMismatch) { v.ok = false; v.reasons += " lms-track-mismatch"; } v.mismatchedSeconds += r.mismatchedSeconds; std::ostringstream stamps, indices, times, titles, elapsed, errors; bool unknown = r.lms.empty(), mismatch = false; for (size_t j = 0; j < r.lms.size(); ++j) { if (j) { stamps << '|'; indices << '|'; times << '|'; titles << '|'; elapsed << '|'; errors << '|'; } const auto &sample = r.lms[j]; ++polls; if (sample.matches.has_value()) ++known; stamps << sample.stamp; indices << sample.index; times << sample.time; titles << cell(sample.title); elapsed << sample.audibleElapsed; errors << sample.timeErrorWall; unknown = unknown || !sample.matches; mismatch = mismatch || (sample.matches && !*sample.matches); } table << r.source.occurrence << '\t' << r.source.rate << '\t' << count(r.source.sourceFrames) << '\t' << r.strm << '\t' << r.decoded << '\t' << r.stmd << '\t' << next << '\t' << r.audible << '\t' << r.stms << '\t' << end << '\t' << lead << '\t' << r.transition << '\t' << r.overlap << '\t' << count(r.promised) << '\t' << r.actual << '\t' << r.prefixFrames << '\t' << r.publication << '\t' << r.fault << '\t' << (r.source.codec == 'f' ? "FLAC" : r.source.codec == 'l' ? "ALAC" : r.source.codec == 'm' ? "MP3" : r.source.codec == 'p' ? "PCM" : r.source.codec == 'a' ? "AAC" : "unknown") << '\t' << (r.audible >= 0 && r.strm >= 0 ? r.audible - r.strm : -1) << '\t' << status << '\t' << r.expectedIndex << '\t' << stamps.str() << '\t' << indices.str() << '\t' << times.str() << '\t' << titles.str() << '\t' << (mismatch ? "false" : unknown ? "unknown" : "true") << '\t' << r.mismatchedSeconds << '\t' << speed << '\t' << (r.strm < 0 ? -1 : r.strm / speed) << '\t' << (r.audible < 0 ? -1 : r.audible / speed) << '\t' << (end < 0 ? -1 : end / speed) << '\t' << (r.decoded < 0 ? -1 : r.decoded / speed) << '\t' << (r.stmd < 0 ? -1 : r.stmd / speed) << '\t' << (next < 0 ? -1 : next / speed) << '\t' << (r.stms < 0 ? -1 : r.stms / speed) << '\t' << (lead < 0 ? -1 : lead / speed) << '\t' << (r.audible >= 0 && r.strm >= 0 ? (r.audible - r.strm) / speed : -1) << '\t' << elapsed.str() << '\t' << errors.str() << '\n'; } if (!v.pairs) { v.ok = false; v.reasons += " zero-completed-pairs"; } if (!known) { v.ok = false; v.reasons += " lms-cli-unavailable"; } if (polls && (polls - known) * 10 > polls) report << "WARNING: LMS CLI unknown for more than 10% of audible polls (" << polls - known << "/" << polls << ").\n\n"; report << "Speed factor: " << speed << "; wall-clock duration: " << wallSeconds << " s.\n\n" << "LMS time is compared with simulated audible elapsed; both are divided by speed for the " "lms_time_error_wall_s diagnostic. STMt already carries simulated elapsed; multiplying " "LMS time again would count speed twice. LMS interpolates between updates in wall time.\n\n"; tsv << table.str(); report << "# Queue mode phase 2: LMS read-ahead\n\nPlayer: Queue Test " "(YeneY), MAC 02:59:51:54:00:01. No " "Sonos.\n\nTimes are simulated monotonic seconds from sink creation; -1 " "means unobserved, not " "zero.\n\n```tsv\n" << table.str() << "```\n\nSuccessor pairs: " << v.pairs << "; worst lead: " << v.worst << " s. Need >=12 s for every completed pair. Incomplete rows: " << v.incomplete << " (run-end, source-fault or flushed).\n\nLMS mismatched seconds: " << v.mismatchedSeconds << ". A sustained mismatch >2 s fails with lms-track-mismatch. CLI " "values are sampled at 1 Hz; " "parallel pipe-separated columns retain every sample. Titles " "are URL-decoded; tabs, newlines, backslashes and pipes are backslash-escaped.\n\nReasons:" << (v.reasons.empty() ? " none" : v.reasons) << "\n\n" << v.line() << '\n'; return v; } } // namespace queue_audio