From 8a41ac7cb95c65d68892810d89c9c80a0463f06d Mon Sep 17 00:00:00 2001 From: skal Date: Fri, 13 Feb 2026 13:17:29 +0100 Subject: Tracker: Sort pattern events + add validation/sanitize modes - tracker_compiler: Sort events by time before C++ generation (required for runtime early-exit optimization) - tracker.cc: Add FATAL_CHECK validating sorted events at init - Add --check mode: Validate .track file without compiling - Add --sanitize mode: Rewrite .track with sorted events and normalized formatting - Fix parser: Skip indented comment lines in patterns All audio tests passing. Co-Authored-By: Claude Sonnet 4.5 --- src/audio/tracker.cc | 16 ++++ tools/tracker_compiler.cc | 235 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 245 insertions(+), 6 deletions(-) diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc index d887208..67c197f 100644 --- a/src/audio/tracker.cc +++ b/src/audio/tracker.cc @@ -3,6 +3,7 @@ #include "audio/synth.h" #include "util/asset_manager.h" #include "util/debug.h" +#include "util/fatal_error.h" #include #include @@ -140,6 +141,21 @@ void tracker_init() { g_tracker_samples_count); #endif /* defined(DEBUG_LOG_TRACKER) */ } + + // Validate that all pattern events are sorted by unit_time + // (required for early-exit optimization in tracker_update) + FATAL_CODE_BEGIN + for (uint32_t pid = 0; pid < g_tracker_patterns_count; ++pid) { + const TrackerPattern& pattern = g_tracker_patterns[pid]; + for (uint32_t i = 1; i < pattern.num_events; ++i) { + FATAL_CHECK(pattern.events[i].unit_time < pattern.events[i - 1].unit_time, + "Pattern %d has unsorted events: event[%d].time=%.3f < " + "event[%d].time=%.3f\n", + pid, i, pattern.events[i].unit_time, i - 1, + pattern.events[i - 1].unit_time); + } + } + FATAL_CODE_END } void tracker_reset() { diff --git a/tools/tracker_compiler.cc b/tools/tracker_compiler.cc index 43b4185..d12005d 100644 --- a/tools/tracker_compiler.cc +++ b/tools/tracker_compiler.cc @@ -1,5 +1,7 @@ +#include #include #include +#include #include #include #include @@ -175,6 +177,145 @@ ResourceAnalysis analyze_resources(const std::vector& samples, return result; } +// Validate and report issues with tracker data +int validate_tracker_data(const std::vector& samples, + const std::vector& patterns, + const std::vector& score, float bpm) { + int warnings = 0; + int errors = 0; + + // Validate BPM + if (bpm <= 0.0f || bpm > 300.0f) { + fprintf(stderr, "WARNING: Unusual BPM value: %.1f\n", bpm); + warnings++; + } + + // Validate samples + for (const auto& s : samples) { + if (s.type == GENERATED) { + if (s.freq <= 0.0f || s.freq > 20000.0f) { + fprintf(stderr, "ERROR: Sample '%s' invalid frequency: %.1f Hz\n", + s.name.c_str(), s.freq); + errors++; + } + if (s.dur <= 0.0f || s.dur > 10.0f) { + fprintf(stderr, "WARNING: Sample '%s' unusual duration: %.2f s\n", + s.name.c_str(), s.dur); + warnings++; + } + } + } + + // Validate patterns + for (const auto& p : patterns) { + if (p.unit_length <= 0.0f) { + fprintf(stderr, "ERROR: Pattern '%s' invalid length: %.2f\n", + p.name.c_str(), p.unit_length); + errors++; + } + + // Check event ordering + for (size_t i = 1; i < p.events.size(); ++i) { + if (p.events[i].unit_time < p.events[i - 1].unit_time) { + fprintf(stderr, + "WARNING: Pattern '%s' has unsorted events: [%zu]=%.3f < " + "[%zu]=%.3f\n", + p.name.c_str(), i, p.events[i].unit_time, i - 1, + p.events[i - 1].unit_time); + warnings++; + } + } + + // Validate event ranges + for (const auto& e : p.events) { + if (e.unit_time < 0.0f || e.unit_time > p.unit_length) { + fprintf(stderr, + "ERROR: Pattern '%s' event time %.3f outside pattern length " + "%.2f\n", + p.name.c_str(), e.unit_time, p.unit_length); + errors++; + } + if (e.volume < 0.0f || e.volume > 2.0f) { + fprintf(stderr, + "WARNING: Pattern '%s' unusual volume: %.2f (expected 0.0-2.0)\n", + p.name.c_str(), e.volume); + warnings++; + } + if (e.pan < -1.0f || e.pan > 1.0f) { + fprintf(stderr, + "ERROR: Pattern '%s' invalid pan: %.2f (must be -1.0 to 1.0)\n", + p.name.c_str(), e.pan); + errors++; + } + } + } + + return errors; +} + +// Write sanitized .track file +void write_sanitized_track(const char* output_path, float bpm, + const std::vector& samples, + std::vector& patterns, + const std::vector& score) { + FILE* out = fopen(output_path, "w"); + if (!out) { + fprintf(stderr, "Could not open output file: %s\n", output_path); + return; + } + + fprintf(out, "# Sanitized tracker file\n"); + fprintf(out, "# Generated by tracker_compiler --sanitize\n\n"); + + fprintf(out, "BPM %.1f\n\n", bpm); + + // Write samples + if (!samples.empty()) { + fprintf(out, "# Samples (%zu total)\n", samples.size()); + for (const auto& s : samples) { + fprintf(out, "SAMPLE %s", s.name.c_str()); + if (s.type == GENERATED) { + fprintf(out, ", %.1f, %.2f, %.1f, %.2f, %d, %.1f", s.freq, s.dur, + s.amp, s.attack, s.harmonics, s.harmonic_decay); + } + fprintf(out, "\n"); + } + fprintf(out, "\n"); + } + + // Write patterns (sorted events) + for (auto& p : patterns) { + // Sort events by time + std::sort(p.events.begin(), p.events.end(), + [](const Event& a, const Event& b) { + return a.unit_time < b.unit_time; + }); + + fprintf(out, "PATTERN %s", p.name.c_str()); + if (p.unit_length != 1.0f) { + fprintf(out, " LENGTH %.2f", p.unit_length); + } + fprintf(out, "\n"); + + for (const auto& e : p.events) { + fprintf(out, " %.4f, %s, %.1f, %.1f\n", e.unit_time, + e.sample_name.c_str(), e.volume, e.pan); + } + fprintf(out, "\n"); + } + + // Write score + if (!score.empty()) { + fprintf(out, "SCORE\n"); + for (const auto& t : score) { + fprintf(out, " %.1f, %s\n", t.time, t.pattern_name.c_str()); + } + } + + fclose(out); + printf("Sanitized track written to: %s\n", output_path); +} + // Write resource analysis to output file void write_resource_analysis(FILE* out, const ResourceAnalysis& analysis, int total_samples) { @@ -206,14 +347,59 @@ void write_resource_analysis(FILE* out, const ResourceAnalysis& analysis, } int main(int argc, char** argv) { - if (argc < 3) { - fprintf(stderr, "Usage: %s \n", argv[0]); + // Parse mode flags + bool check_mode = false; + bool sanitize_mode = false; + const char* input_file = nullptr; + const char* output_file = nullptr; + + int arg_idx = 1; + while (arg_idx < argc) { + if (strcmp(argv[arg_idx], "--check") == 0) { + check_mode = true; + arg_idx++; + } else if (strcmp(argv[arg_idx], "--sanitize") == 0) { + sanitize_mode = true; + arg_idx++; + } else if (!input_file) { + input_file = argv[arg_idx]; + arg_idx++; + } else if (!output_file) { + output_file = argv[arg_idx]; + arg_idx++; + } else { + fprintf(stderr, "Unexpected argument: %s\n", argv[arg_idx]); + return 1; + } + } + + // Validate arguments + if (!input_file) { + fprintf(stderr, "Usage:\n"); + fprintf(stderr, " %s # Compile\n", + argv[0]); + fprintf(stderr, " %s --check # Validate only\n", + argv[0]); + fprintf(stderr, + " %s --sanitize # Sanitize\n", + argv[0]); + return 1; + } + + if (!check_mode && !output_file) { + fprintf(stderr, "Error: Output file required for compile/sanitize mode\n"); + fprintf(stderr, "Use --check for validation only\n"); + return 1; + } + + if (check_mode && sanitize_mode) { + fprintf(stderr, "Error: Cannot use --check and --sanitize together\n"); return 1; } - std::ifstream in(argv[1]); + std::ifstream in(input_file); if (!in.is_open()) { - fprintf(stderr, "Could not open input file: %s\n", argv[1]); + fprintf(stderr, "Could not open input file: %s\n", input_file); return 1; } @@ -235,6 +421,10 @@ int main(int argc, char** argv) { std::string cmd; ss >> cmd; + // Skip comment lines (including indented comments) + if (cmd.empty() || cmd[0] == '#') + continue; + if (cmd == "BPM") { ss >> bpm; } else if (cmd == "SAMPLE") { @@ -330,9 +520,34 @@ int main(int argc, char** argv) { } } - FILE* out_file = fopen(argv[2], "w"); + // Validate tracker data + int errors = validate_tracker_data(samples, patterns, score, bpm); + if (errors > 0) { + fprintf(stderr, "\nValidation failed with %d errors\n", errors); + if (check_mode) { + return 1; + } + // Continue compilation with warnings + } + + // Handle different modes + if (check_mode) { + printf("Validation passed for: %s\n", input_file); + printf(" Patterns: %zu\n", patterns.size()); + printf(" Score triggers: %zu\n", score.size()); + printf(" Samples: %zu\n", samples.size()); + return 0; + } + + if (sanitize_mode) { + write_sanitized_track(output_file, bpm, samples, patterns, score); + return 0; + } + + // Normal compilation mode + FILE* out_file = fopen(output_file, "w"); if (!out_file) { - fprintf(stderr, "Could not open output file: %s\n", argv[2]); + fprintf(stderr, "Could not open output file: %s\n", output_file); return 1; } fprintf(out_file, "// Generated by tracker_compiler. Do not edit.\n\n"); @@ -366,6 +581,14 @@ int main(int argc, char** argv) { } fprintf(out_file, "};\n\n"); + // Sort pattern events by time (required by runtime early-exit optimization) + for (auto& p : patterns) { + std::sort(p.events.begin(), p.events.end(), + [](const Event& a, const Event& b) { + return a.unit_time < b.unit_time; + }); + } + for (const auto& p : patterns) { fprintf(out_file, "static const TrackerEvent PATTERN_EVENTS_%s[] = {\n", p.name.c_str()); -- cgit v1.2.3