@@ -0,0 +1,234 @@
|
||||
// Headless check of the preferences dialog resource inside the built component.
|
||||
//
|
||||
// prefs_layout_check <path to foo_input_joc.dll>
|
||||
//
|
||||
// The page's appearance cannot be inspected on this machine (windows created from
|
||||
// an agent shell are not on an enumerable desktop), but every property that makes
|
||||
// a page look or behave wrong *is* measurable:
|
||||
//
|
||||
// * a WS_CAPTION / WS_BORDER on an embedded page draws a second frame inside the
|
||||
// host's own frame -- that is the "two nested pages" look;
|
||||
// * a control whose centre does not hit-test to itself cannot be clicked, and
|
||||
// WindowFromPoint says which window is swallowing the click (usually a group
|
||||
// box declared after it, or the host's clip region when the control sits
|
||||
// outside the dialog's client area);
|
||||
// * controls outside the dialog's client rectangle are invisible or cut off;
|
||||
// * disabled controls are reported so "I can see it but cannot change it" can be
|
||||
// told apart from "it is covered".
|
||||
//
|
||||
// It creates the dialog from the resource exactly as the component does, inside a
|
||||
// host window, and prints one line per control.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
struct Control {
|
||||
HWND window = nullptr;
|
||||
int id = 0;
|
||||
std::string window_class;
|
||||
std::string text;
|
||||
bool top_level = false; // direct child of the dialog
|
||||
};
|
||||
|
||||
std::string narrow(const wchar_t* text) {
|
||||
if (text == nullptr || *text == L'\0') return {};
|
||||
const int needed =
|
||||
WideCharToMultiByte(CP_UTF8, 0, text, -1, nullptr, 0, nullptr, nullptr);
|
||||
std::string out(static_cast<std::size_t>(needed), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, text, -1, out.data(), needed, nullptr, nullptr);
|
||||
if (!out.empty() && out.back() == '\0') out.pop_back();
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string class_of(HWND window) {
|
||||
wchar_t buffer[256] = {};
|
||||
GetClassNameW(window, buffer, 255);
|
||||
return narrow(buffer);
|
||||
}
|
||||
|
||||
std::string text_of(HWND window) {
|
||||
const int length = GetWindowTextLengthW(window);
|
||||
if (length <= 0) return {};
|
||||
std::wstring buffer(static_cast<std::size_t>(length) + 1, L'\0');
|
||||
GetWindowTextW(window, buffer.data(), length + 1);
|
||||
buffer.resize(static_cast<std::size_t>(length));
|
||||
return narrow(buffer.c_str());
|
||||
}
|
||||
|
||||
BOOL CALLBACK collect(HWND window, LPARAM param) {
|
||||
auto* controls = reinterpret_cast<std::vector<Control>*>(param);
|
||||
Control control;
|
||||
control.window = window;
|
||||
control.id = GetDlgCtrlID(window);
|
||||
control.window_class = class_of(window);
|
||||
control.text = text_of(window);
|
||||
controls->push_back(control);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
INT_PTR CALLBACK page_proc(HWND, UINT, WPARAM, LPARAM) { return FALSE; }
|
||||
|
||||
HWND create_host() {
|
||||
static const wchar_t* kClass = L"joc_prefs_host";
|
||||
WNDCLASSEXW wc{};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.lpfnWndProc = DefWindowProcW;
|
||||
wc.hInstance = GetModuleHandleW(nullptr);
|
||||
wc.lpszClassName = kClass;
|
||||
RegisterClassExW(&wc);
|
||||
return CreateWindowExW(0, kClass, L"host", WS_OVERLAPPEDWINDOW, 0, 0, 640, 480, nullptr,
|
||||
nullptr, wc.hInstance, nullptr);
|
||||
}
|
||||
|
||||
bool is_descendant(HWND candidate, HWND ancestor) {
|
||||
for (HWND walk = candidate; walk != nullptr; walk = GetParent(walk)) {
|
||||
if (walk == ancestor) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A control the user can operate: static text and group boxes answer WM_NCHITTEST
|
||||
// with HTTRANSPARENT, so a real click passes straight through them.
|
||||
bool is_interactive(HWND window) {
|
||||
const std::string window_class = class_of(window);
|
||||
if (window_class == "Edit" || window_class == "ComboBox") return true;
|
||||
if (window_class != "Button") return false;
|
||||
const LONG_PTR style = GetWindowLongPtrW(window, GWL_STYLE);
|
||||
return (style & BS_GROUPBOX) == 0;
|
||||
}
|
||||
|
||||
// True when another interactive sibling sits above the control at that point --
|
||||
// that, and only that, is what stops a click from reaching it. The walk goes
|
||||
// from the top of the z-order downwards (GW_CHILD / GW_HWNDNEXT), which is the
|
||||
// order Windows itself hit-tests in.
|
||||
bool covered_by_sibling(HWND dialog, HWND control, POINT centre) {
|
||||
for (HWND walk = GetWindow(dialog, GW_CHILD); walk != nullptr;
|
||||
walk = GetWindow(walk, GW_HWNDNEXT)) {
|
||||
if (walk == control) return false; // reached it: nothing above is in the way
|
||||
if (!is_interactive(walk) || IsWindowVisible(walk) == FALSE) continue;
|
||||
RECT rect{};
|
||||
GetWindowRect(walk, &rect);
|
||||
POINT top_left{rect.left, rect.top};
|
||||
POINT bottom_right{rect.right, rect.bottom};
|
||||
ScreenToClient(dialog, &top_left);
|
||||
ScreenToClient(dialog, &bottom_right);
|
||||
if (centre.x >= top_left.x && centre.x < bottom_right.x && centre.y >= top_left.y &&
|
||||
centre.y < bottom_right.y) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 2) {
|
||||
std::fprintf(stderr, "usage: prefs_layout_check <foo_input_joc.dll>\n");
|
||||
return 2;
|
||||
}
|
||||
const std::wstring dll_path = [&] {
|
||||
const int needed = MultiByteToWideChar(CP_UTF8, 0, argv[1], -1, nullptr, 0);
|
||||
std::wstring out(static_cast<std::size_t>(needed), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, argv[1], -1, out.data(), needed);
|
||||
if (!out.empty() && out.back() == L'\0') out.pop_back();
|
||||
return out;
|
||||
}();
|
||||
|
||||
HMODULE module = LoadLibraryExW(dll_path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);
|
||||
if (module == nullptr) {
|
||||
std::fprintf(stderr, "cannot load %s (error %lu)\n", argv[1], GetLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
HWND host = create_host();
|
||||
HWND dialog = CreateDialogParamW(module, MAKEINTRESOURCEW(2001), host, page_proc, 0);
|
||||
if (dialog == nullptr) {
|
||||
std::fprintf(stderr, "CreateDialogParamW failed (error %lu)\n", GetLastError());
|
||||
return 1;
|
||||
}
|
||||
ShowWindow(host, SW_SHOW);
|
||||
ShowWindow(dialog, SW_SHOW);
|
||||
UpdateWindow(dialog);
|
||||
|
||||
const LONG_PTR style = GetWindowLongPtrW(dialog, GWL_STYLE);
|
||||
RECT window_rect{};
|
||||
RECT client_rect{};
|
||||
GetWindowRect(dialog, &window_rect);
|
||||
GetClientRect(dialog, &client_rect);
|
||||
const int border_x = static_cast<int>((window_rect.right - window_rect.left) -
|
||||
(client_rect.right - client_rect.left));
|
||||
const int border_y = static_cast<int>((window_rect.bottom - window_rect.top) -
|
||||
(client_rect.bottom - client_rect.top));
|
||||
|
||||
std::printf("dialog 0x%p client=%ldx%ld non-client=%dx%d\n", static_cast<void*>(dialog),
|
||||
client_rect.right, client_rect.bottom, border_x, border_y);
|
||||
std::printf(" WS_CAPTION=%s WS_BORDER=%s WS_CHILD=%s WS_VISIBLE=%s WS_TABSTOP=%s\n",
|
||||
(style & WS_CAPTION) ? "yes" : "no", (style & WS_BORDER) ? "yes" : "no",
|
||||
(style & WS_CHILD) ? "yes" : "no", (style & WS_VISIBLE) ? "yes" : "no",
|
||||
(style & WS_TABSTOP) ? "yes" : "no");
|
||||
if ((style & WS_CAPTION) != 0) {
|
||||
std::printf(" NOTE: a caption on an embedded page is the second frame the host "
|
||||
"already draws\n");
|
||||
}
|
||||
|
||||
std::vector<Control> controls;
|
||||
EnumChildWindows(dialog, collect, reinterpret_cast<LPARAM>(&controls));
|
||||
|
||||
std::printf("\n%-28s %-22s %-8s %-7s %-9s %-16s %s\n", "text", "class", "id", "enabled",
|
||||
"inside", "hit-test", "covered by");
|
||||
int problems = 0;
|
||||
LONG max_x = 0;
|
||||
LONG max_y = 0;
|
||||
for (const Control& control : controls) {
|
||||
RECT rect{};
|
||||
GetWindowRect(control.window, &rect);
|
||||
POINT top_left{rect.left, rect.top};
|
||||
POINT bottom_right{rect.right, rect.bottom};
|
||||
ScreenToClient(dialog, &top_left);
|
||||
ScreenToClient(dialog, &bottom_right);
|
||||
if (bottom_right.x > max_x) max_x = bottom_right.x;
|
||||
if (bottom_right.y > max_y) max_y = bottom_right.y;
|
||||
|
||||
const bool inside = top_left.x >= 0 && top_left.y >= 0 &&
|
||||
bottom_right.x <= client_rect.right &&
|
||||
bottom_right.y <= client_rect.bottom;
|
||||
const bool enabled = IsWindowEnabled(control.window) != FALSE;
|
||||
|
||||
// Hit-test the way Windows does for a real click: a static or a group box
|
||||
// in the way is transparent, another interactive control is not.
|
||||
POINT centre{(top_left.x + bottom_right.x) / 2, (top_left.y + bottom_right.y) / 2};
|
||||
const bool blocked = covered_by_sibling(dialog, control.window, centre);
|
||||
const bool hits_self = !blocked;
|
||||
std::string covered = blocked ? std::string("another control") : std::string();
|
||||
|
||||
// Only controls the user is meant to operate count as problems: static
|
||||
// text and group boxes are transparent to the mouse by design.
|
||||
const LONG_PTR control_style = GetWindowLongPtrW(control.window, GWL_STYLE);
|
||||
const bool is_group_box =
|
||||
control.window_class == "Button" && (control_style & BS_GROUPBOX) != 0;
|
||||
const bool is_label = control.window_class == "Static" && !is_group_box;
|
||||
const bool interactive = !is_group_box && !is_label;
|
||||
const bool bad = interactive && (!enabled || !inside || !hits_self);
|
||||
if (bad) ++problems;
|
||||
|
||||
std::printf("%-28s %-22s %-8d %-7s %-9s %-16s %s%s\n",
|
||||
control.text.empty() ? "(no text)" : control.text.c_str(),
|
||||
control.window_class.c_str(), control.id, enabled ? "yes" : "NO",
|
||||
inside ? "yes" : "NO", hits_self ? "self" : "OTHER", covered.c_str(),
|
||||
bad ? " <== PROBLEM" : (interactive ? "" : " (label)"));
|
||||
}
|
||||
|
||||
std::printf("\ncontent extent=%ldx%ld client=%ldx%ld %s\n", max_x, max_y,
|
||||
client_rect.right, client_rect.bottom,
|
||||
(max_x <= client_rect.right && max_y <= client_rect.bottom)
|
||||
? "(everything fits)"
|
||||
: "(CONTENT IS CLIPPED)");
|
||||
std::printf("controls=%zu problems=%d\n", controls.size(), problems);
|
||||
return problems == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Headless render harness: drives the plugin's own decode engine and writes a WAV
|
||||
// in the exact header format the reference CLI writes, so the two outputs can be
|
||||
// compared byte for byte.
|
||||
//
|
||||
// render_harness --input <file.eac3> --output <out.wav> [options]
|
||||
// --mode binaural|speaker default binaural
|
||||
// --layout NAME speaker layout (default 7.1)
|
||||
// --hrtf-source sofa|rosella kind of model --hrtf points at (default sofa)
|
||||
// --hrtf PATH HRTF to render with; supplied by the caller and
|
||||
// never part of this repository. Without it the
|
||||
// default location beside the executable is used.
|
||||
// Speaker layouts need no HRTF.
|
||||
// --gain-db X default 0
|
||||
// --tail S binaural tail seconds (default 5)
|
||||
// --ffmpeg PATH default ffmpeg
|
||||
// --max-frames N stop after N output frames (0 = all)
|
||||
//
|
||||
// It exists because the plugin's engine (src/joc_decode.*) has no foobar2000
|
||||
// dependency: the very code that plays in foobar2000 can be run here and its
|
||||
// output compared against the reference renderer, which is what the bit-exactness
|
||||
// contract is about. The WAV header mirrors the renderer's own writer: a simple
|
||||
// IEEE-float header up to two channels, WAVE_FORMAT_EXTENSIBLE above that with a
|
||||
// channel mask of zero.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../src/joc_decode.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct Options {
|
||||
std::string input;
|
||||
std::string output;
|
||||
std::string mode = "binaural";
|
||||
std::string layout = "7.1";
|
||||
std::string hrtf; // SOFA file, or Rosella model file
|
||||
std::string hrtf_source = "sofa"; // sofa | rosella
|
||||
std::string ffmpeg = "ffmpeg";
|
||||
double gain_db = 0.0;
|
||||
double tail_seconds = 5.0;
|
||||
std::uint64_t max_frames = 0;
|
||||
std::uint64_t max_input_frames = 0;
|
||||
};
|
||||
|
||||
void put_u16(std::string* out, unsigned value) {
|
||||
out->push_back(static_cast<char>(value & 0xFFu));
|
||||
out->push_back(static_cast<char>((value >> 8) & 0xFFu));
|
||||
}
|
||||
|
||||
void put_u32(std::string* out, unsigned long long value) {
|
||||
for (int i = 0; i < 4; ++i) out->push_back(static_cast<char>((value >> (8 * i)) & 0xFFu));
|
||||
}
|
||||
|
||||
// Same bytes the core's wav_writer produces.
|
||||
std::string wav_header(unsigned channels, unsigned rate, std::uint64_t frames) {
|
||||
const unsigned bytes_per_sample = 4;
|
||||
const unsigned block_align = channels * bytes_per_sample;
|
||||
const std::uint64_t data_bytes = frames * block_align;
|
||||
|
||||
std::string fmt;
|
||||
if (channels <= 2) {
|
||||
put_u16(&fmt, 3); // WAVE_FORMAT_IEEE_FLOAT
|
||||
put_u16(&fmt, channels);
|
||||
put_u32(&fmt, rate);
|
||||
put_u32(&fmt, rate * block_align);
|
||||
put_u16(&fmt, block_align);
|
||||
put_u16(&fmt, 8 * bytes_per_sample);
|
||||
} else {
|
||||
put_u16(&fmt, 0xFFFE); // WAVE_FORMAT_EXTENSIBLE
|
||||
put_u16(&fmt, channels);
|
||||
put_u32(&fmt, rate);
|
||||
put_u32(&fmt, rate * block_align);
|
||||
put_u16(&fmt, block_align);
|
||||
put_u16(&fmt, 8 * bytes_per_sample);
|
||||
put_u16(&fmt, 22); // cbSize
|
||||
put_u16(&fmt, 8 * bytes_per_sample);
|
||||
put_u32(&fmt, 0); // channel mask, as the core writes it
|
||||
static const unsigned char kFloatGuid[16] = {0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
|
||||
0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71};
|
||||
fmt.append(reinterpret_cast<const char*>(kFloatGuid), 16);
|
||||
}
|
||||
|
||||
std::string header;
|
||||
header.append("RIFF", 4);
|
||||
put_u32(&header, 4u + 8u + fmt.size() + 8u + data_bytes);
|
||||
header.append("WAVE", 4);
|
||||
header.append("fmt ", 4);
|
||||
put_u32(&header, fmt.size());
|
||||
header.append(fmt);
|
||||
header.append("data", 4);
|
||||
put_u32(&header, data_bytes);
|
||||
return header;
|
||||
}
|
||||
|
||||
bool parse(int argc, char** argv, Options* options) {
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::string arg = argv[i];
|
||||
auto next = [&]() -> std::string { return (i + 1 < argc) ? argv[++i] : std::string(); };
|
||||
if (arg == "--input") options->input = next();
|
||||
else if (arg == "--output") options->output = next();
|
||||
else if (arg == "--mode") options->mode = next();
|
||||
else if (arg == "--layout") options->layout = next();
|
||||
else if (arg == "--hrtf") options->hrtf = next();
|
||||
else if (arg == "--hrtf-source") options->hrtf_source = next();
|
||||
else if (arg == "--ffmpeg") options->ffmpeg = next();
|
||||
else if (arg == "--gain-db") options->gain_db = std::atof(next().c_str());
|
||||
else if (arg == "--tail") options->tail_seconds = std::atof(next().c_str());
|
||||
else if (arg == "--max-frames") options->max_frames = std::strtoull(next().c_str(), nullptr, 10);
|
||||
else if (arg == "--max-input-frames") options->max_input_frames = std::strtoull(next().c_str(), nullptr, 10);
|
||||
else if (arg == "--help" || arg == "-h") return false;
|
||||
else {
|
||||
std::fprintf(stderr, "render_harness: unknown argument %s\n", arg.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !options->input.empty() && !options->output.empty();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Options options;
|
||||
if (!parse(argc, argv, &options)) {
|
||||
std::fprintf(stderr,
|
||||
"usage: render_harness --input <eac3> --output <wav> [--mode binaural|speaker]\n"
|
||||
" [--layout NAME] [--hrtf-source sofa|rosella] [--hrtf PATH]\n"
|
||||
" [--gain-db X] [--tail S] [--ffmpeg path] [--max-frames N]\n"
|
||||
" [--max-input-frames N]\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
joc_decode::Settings settings;
|
||||
settings.output = (options.mode == "speaker") ? joc_decode::Output::kSpeaker
|
||||
: joc_decode::Output::kBinaural;
|
||||
settings.speaker_layout = options.layout;
|
||||
settings.hrtf_source = (options.hrtf_source == "rosella") ? joc_decode::HrtfSource::kRosella
|
||||
: joc_decode::HrtfSource::kSofa;
|
||||
settings.hrtf_file = options.hrtf;
|
||||
settings.gain_db = options.gain_db;
|
||||
settings.tail_seconds = options.tail_seconds;
|
||||
settings.ffmpeg_path = options.ffmpeg;
|
||||
settings.input_frame_limit = options.max_input_frames;
|
||||
|
||||
joc_decode::Engine engine;
|
||||
std::string error;
|
||||
if (!engine.start(options.input, settings, &error)) {
|
||||
std::fprintf(stderr, "render_harness: engine start failed: %s\n", error.c_str());
|
||||
return 1;
|
||||
}
|
||||
const unsigned channels = engine.channels();
|
||||
if (channels == 0) {
|
||||
std::fprintf(stderr, "render_harness: engine reported zero channels\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::FILE* file = std::fopen(options.output.c_str(), "wb");
|
||||
if (file == nullptr) {
|
||||
std::fprintf(stderr, "render_harness: cannot write %s\n", options.output.c_str());
|
||||
return 1;
|
||||
}
|
||||
// The header carries the length, so write a placeholder and come back to it.
|
||||
const std::string header = wav_header(channels, 48000, 0);
|
||||
std::fwrite(header.data(), 1, header.size(), file);
|
||||
|
||||
constexpr std::size_t kChunk = 4096;
|
||||
std::vector<float> buffer(kChunk * channels);
|
||||
std::uint64_t frames_written = 0;
|
||||
double peak = 0.0;
|
||||
for (;;) {
|
||||
std::size_t want = kChunk;
|
||||
if (options.max_frames != 0) {
|
||||
if (frames_written >= options.max_frames) break;
|
||||
const std::uint64_t left = options.max_frames - frames_written;
|
||||
if (left < want) want = static_cast<std::size_t>(left);
|
||||
}
|
||||
const std::size_t frames = engine.read(buffer.data(), want, &error);
|
||||
if (frames == 0) {
|
||||
if (!error.empty()) {
|
||||
std::fprintf(stderr, "render_harness: read failed: %s\n", error.c_str());
|
||||
std::fclose(file);
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
for (std::size_t i = 0; i < frames * channels; ++i) {
|
||||
const double value = buffer[i] < 0.0f ? -static_cast<double>(buffer[i])
|
||||
: static_cast<double>(buffer[i]);
|
||||
if (value > peak) peak = value;
|
||||
}
|
||||
std::fwrite(buffer.data(), sizeof(float), frames * channels, file);
|
||||
frames_written += frames;
|
||||
}
|
||||
engine.stop();
|
||||
|
||||
const std::string final_header = wav_header(channels, 48000, frames_written);
|
||||
std::fseek(file, 0, SEEK_SET);
|
||||
std::fwrite(final_header.data(), 1, final_header.size(), file);
|
||||
std::fclose(file);
|
||||
|
||||
std::printf("render_harness: %s -> %s\n", options.input.c_str(), options.output.c_str());
|
||||
std::printf(" channels=%u frames=%llu samples_per_channel=%llu peak=%.9f\n", channels,
|
||||
static_cast<unsigned long long>(frames_written),
|
||||
static_cast<unsigned long long>(frames_written), peak);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Cross-validation of the plugin's bitstream gate against the rendering core.
|
||||
//
|
||||
// scan_crosscheck <file.eac3> [more files...]
|
||||
//
|
||||
// For every syncframe in the first part of each file this compares
|
||||
// * the frame length the plugin derives from frmsiz, against joc_eac3_frame_bytes()
|
||||
// * the plugin's JOC verdict, against whether joc_parse_eac3_frame() succeeds
|
||||
//
|
||||
// joc_parse_eac3_frame() is the core's [T2] verification tier. A media host must
|
||||
// not build on that tier, which is exactly why the plugin has its own gate -- but
|
||||
// it is the right reference for proving the gate agrees with the renderer, and
|
||||
// that is what this tool is for.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "joc_core.h"
|
||||
|
||||
#include "../src/eac3_scan.h"
|
||||
|
||||
namespace {
|
||||
|
||||
bool kernel_accepts_frame(const unsigned char* frame, std::size_t frame_bytes,
|
||||
const char** reason) {
|
||||
joc_frame_params params{};
|
||||
params.struct_size = sizeof(params);
|
||||
params.struct_version = JOC_FRAME_PARAMS_VERSION;
|
||||
joc_emdf_info emdf{};
|
||||
emdf.struct_size = sizeof(emdf);
|
||||
emdf.struct_version = JOC_EMDF_INFO_VERSION;
|
||||
|
||||
const joc_error error = joc_parse_eac3_frame(frame, frame_bytes, ¶ms, &emdf);
|
||||
if (error == JOC_OK) return true;
|
||||
if (reason != nullptr) *reason = joc_error_name(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 2) {
|
||||
std::fprintf(stderr, "usage: scan_crosscheck <file> [more files...]\n");
|
||||
return 2;
|
||||
}
|
||||
int mismatches = 0;
|
||||
int files = 0;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::FILE* handle = std::fopen(argv[i], "rb");
|
||||
if (handle == nullptr) {
|
||||
std::fprintf(stderr, "%s: cannot open\n", argv[i]);
|
||||
++mismatches;
|
||||
continue;
|
||||
}
|
||||
std::vector<unsigned char> buffer(256u * 1024u);
|
||||
const std::size_t size = std::fread(buffer.data(), 1, buffer.size(), handle);
|
||||
std::fclose(handle);
|
||||
++files;
|
||||
|
||||
std::size_t offset = 0;
|
||||
std::size_t frames = 0;
|
||||
std::size_t length_mismatch = 0;
|
||||
std::size_t verdict_mismatch = 0;
|
||||
std::size_t plugin_yes = 0;
|
||||
std::size_t kernel_yes = 0;
|
||||
const char* first_reason = "";
|
||||
|
||||
while (frames < 64) {
|
||||
const std::size_t mine = joc_eac3::frame_bytes_at(buffer.data(), size, offset);
|
||||
if (mine == 0) break;
|
||||
if (offset + mine > size) break;
|
||||
|
||||
std::size_t theirs = 0;
|
||||
const joc_error length_error = joc_eac3_frame_bytes(buffer.data(), size, offset, &theirs);
|
||||
if (length_error != JOC_OK || theirs != mine) ++length_mismatch;
|
||||
|
||||
const bool plugin = joc_eac3::frame_has_joc(buffer.data() + offset, mine);
|
||||
const char* reason = "";
|
||||
const bool kernel = kernel_accepts_frame(buffer.data() + offset, mine, &reason);
|
||||
if (plugin != kernel) {
|
||||
if (verdict_mismatch == 0) first_reason = reason;
|
||||
++verdict_mismatch;
|
||||
}
|
||||
plugin_yes += plugin ? 1u : 0u;
|
||||
kernel_yes += kernel ? 1u : 0u;
|
||||
|
||||
++frames;
|
||||
offset += mine;
|
||||
}
|
||||
|
||||
std::printf("%-34s frames=%-3llu plugin_joc=%-3llu kernel_joc=%-3llu len_mismatch=%llu verdict_mismatch=%llu\n",
|
||||
argv[i], static_cast<unsigned long long>(frames),
|
||||
static_cast<unsigned long long>(plugin_yes),
|
||||
static_cast<unsigned long long>(kernel_yes),
|
||||
static_cast<unsigned long long>(length_mismatch),
|
||||
static_cast<unsigned long long>(verdict_mismatch));
|
||||
if (verdict_mismatch != 0) {
|
||||
std::printf("%-34s first kernel error on a disagreeing frame: %s\n", "", first_reason);
|
||||
}
|
||||
mismatches += static_cast<int>(length_mismatch + verdict_mismatch);
|
||||
}
|
||||
|
||||
std::printf("crosscheck: %d file(s), %s\n", files, mismatches == 0 ? "AGREES WITH CORE" : "DISAGREES");
|
||||
return mismatches == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Standalone self-test for the JOC bitstream gate.
|
||||
//
|
||||
// scan_selftest <file.eac3> [more files...]
|
||||
//
|
||||
// Prints, per file, how many of the first syncframes carry the JOC EMDF container.
|
||||
// Run it against a known JOC stream and against one whose extension has been
|
||||
// stripped (ffmpeg -c copy -bsf:a eac3_core): the first must report every frame
|
||||
// with JOC, the second none.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
#include "../src/eac3_scan.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 2) {
|
||||
std::fprintf(stderr, "usage: scan_selftest <file> [more files...]\n");
|
||||
return 2;
|
||||
}
|
||||
int failures = 0;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::FILE* file = std::fopen(argv[i], "rb");
|
||||
if (file == nullptr) {
|
||||
std::fprintf(stderr, "%s: cannot open\n", argv[i]);
|
||||
++failures;
|
||||
continue;
|
||||
}
|
||||
std::vector<unsigned char> buffer(256u * 1024u);
|
||||
const std::size_t got = std::fread(buffer.data(), 1, buffer.size(), file);
|
||||
std::fclose(file);
|
||||
|
||||
const joc_eac3::ScanResult result = joc_eac3::scan(buffer.data(), got, 64);
|
||||
const char* state = "unknown";
|
||||
if (result.joc == joc_eac3::JocState::kYes) state = "JOC";
|
||||
else if (result.joc == joc_eac3::JocState::kNo) state = "not-JOC";
|
||||
|
||||
std::printf("%-28s %-8s frames=%llu with_joc=%llu first_frame=%llu same_size=%s\n",
|
||||
argv[i], state, static_cast<unsigned long long>(result.frames_examined),
|
||||
static_cast<unsigned long long>(result.frames_with_joc),
|
||||
static_cast<unsigned long long>(result.first_frame_bytes),
|
||||
result.all_frames_same_size ? "yes" : "no");
|
||||
std::printf("%-28s %s\n", "", result.detail);
|
||||
}
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user