From 948d21d25e97c443c4037b77ebb52806d98fdd02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Mon, 3 Aug 2026 23:04:08 -0400 Subject: [PATCH] halp: one quantification grid, walked once, matching libossia exactly tick_musical reported no quantification date at all when the timeline ran backwards, and its sub-bar grid ran from the start of the tick rather than from the bar line, so a bar whose length is not a whole number of divisions carried a stale phase into the next one. Walk one grid instead: bar lines come from the signature and from the bar positions the host reports, the grid restarts at each of them, and the walk runs in tick order in both directions - [start; end[ forwards, ]end; start] rewinding - so a point on the tick's far end belongs to the next tick and fires exactly once. metronome() and get_quantification_date() now share that walk and one map from a musical position to a frame, so a click and a quantized event on the same bar line land on the same sample. The result matches ossia::token_request point for point: over five signatures, eight rates, four speeds and two buffer lengths the two agree exactly on the count, the index and the frame of every point, which ossia/score's QuantificationParityTest now asserts strictly. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XGta2LPAedDP3Txvyq8kTW --- cmake/avendish.tests.cmake | 1 + include/halp/audio.hpp | 712 ++++++++++++------------------------- tests/quantification.cpp | 193 ++++++++++ 3 files changed, 425 insertions(+), 481 deletions(-) create mode 100644 tests/quantification.cpp diff --git a/cmake/avendish.tests.cmake b/cmake/avendish.tests.cmake index 680369723..276bab5d9 100644 --- a/cmake/avendish.tests.cmake +++ b/cmake/avendish.tests.cmake @@ -46,6 +46,7 @@ if(BUILD_TESTING) avnd_add_executable_test(test_introspection_many tests/test_introspection_many.cpp) avnd_add_executable_test(test_reflection tests/test_reflection.cpp) + avnd_add_catch_test(test_quantification tests/quantification.cpp) avnd_add_catch_test(test_gain tests/objects/gain.cpp) avnd_add_catch_test(test_patternal tests/objects/patternal.cpp) diff --git a/include/halp/audio.hpp b/include/halp/audio.hpp index f876860ba..bf0d37d22 100644 --- a/include/halp/audio.hpp +++ b/include/halp/audio.hpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -311,550 +312,299 @@ struct tick_musical quarter_note bar_at_end{}; // Position in bar to frames - constexpr auto pos_to_frame(double in_bar) const noexcept + constexpr int pos_to_frame(double in_bar) const noexcept { - double start = start_position_in_quarters; - double musical_pos = in_bar + bar_at_start; - double end = end_position_in_quarters; - - double percent = (musical_pos - start) / (end - start); - int f = percent * this->frames; - return f; + const double start = start_position_in_quarters; + const double musical_pos = in_bar + bar_at_start; + const double end = end_position_in_quarters; + + const double duration = end - start; + if(duration == 0. || frames <= 0) + return 0; + + // Rewinding, end is before start and the numerator is negative too, so the + // ratio still grows with the buffer position. + const double percent = (musical_pos - start) / duration; + const int f = percent * this->frames; + return std::clamp(f, 0, frames - 1); } constexpr int64_t prev_frame() const noexcept { return position_in_frames; } constexpr int64_t end_frame() const noexcept { return position_in_frames + frames; } - //! Given a quantification rate (1 for bars, 2 for half, 4 for quarters...) - //! return all occurring quantification dates in the tick - [[nodiscard]] quantification_frames get_quantification_date(double rate) const noexcept + //! Calls fn(bar_line, next_bar_line) for every bar segment the tick touches, + //! in increasing musical order. + //! + //! Bar lines come from two places and both matter: the ones the signature + //! implies, and the one reported for the far end of the tick. A signature + //! change puts a bar line where the arithmetic alone would not, and the grid + //! restarts there. + //! + //! Kept identical to ossia::token_request::for_each_bar_segment: a plugin and + //! a native node on the same score have to snap to the same grid. + template + void for_each_bar_segment(double lo, double hi, F&& fn) const noexcept { - quantification_frames result; - - if(prev_frame() == end_frame()) - return result; - - if(rate <= 0.) + const bool valid_sig = signature.num > 0 && signature.denom > 0; + const double quarters_in_bar + = valid_sig ? 4. * signature.num / signature.denom : 4.; + if(!(quarters_in_bar > 0.)) + return; + + constexpr double eps = 1e-9; + const bool rewinding = end_position_in_quarters < start_position_in_quarters; + const double near_bar = rewinding ? bar_at_end : bar_at_start; + const double far_bar = rewinding ? bar_at_start : bar_at_end; + + double b = near_bar; + for(int i = 0; i < 1024 && b < far_bar - eps; i++) { - result.emplace_back(0, 0); - return result; + const double next + = (b + quarters_in_bar < far_bar) ? b + quarters_in_bar : far_bar; + if(next > lo + eps && b < hi + eps) + fn(b, next); + b += quarters_in_bar; } - const double musical_tick_duration - = end_position_in_quarters - start_position_in_quarters; - if(musical_tick_duration <= 0.) + b = (far_bar > near_bar) ? far_bar : near_bar; + for(int i = 0; i < 1024 && b < hi + eps; i++) { - result.emplace_back(0, 0); - return result; + fn(b, b + quarters_in_bar); + b += quarters_in_bar; } + } - if(rate <= 1.) - { - // Bars or longer - use logic from get_quantification_date_for_bars_or_longer - const double bars_per_quantization = 1.0 / rate; - - // Convert positions to bar numbers from the last signature - const double start_bar_position - = (start_position_in_quarters - last_signature_change) - / (4.0 * signature.num / signature.denom); - const double end_bar_position = (end_position_in_quarters - last_signature_change) - / (4.0 * signature.num / signature.denom); - - // Check if we're exactly on a quantization point at the start - const double start_remainder - = std::fmod(start_bar_position, bars_per_quantization); - if(std::abs(start_remainder) < 0.0001 && start_position_in_quarters >= 0) - { - result.emplace_back( - 0, static_cast(std::round(start_bar_position / bars_per_quantization))); - } + //! Every quantification point the tick crosses, in tick order, as a frame + //! offset into the buffer and the index of the point within its bar. + [[nodiscard]] quantification_frames get_quantification_date(double rate) const noexcept + { + quantification_frames res; - // Find all quantization points after start and before end - const double start_quant_bar - = std::floor(start_bar_position / bars_per_quantization); - double next_quant_bar_number = (start_quant_bar + 1) * bars_per_quantization; + if(prev_frame() == end_frame()) + return res; - while(next_quant_bar_number < end_bar_position) - { - // Calculate the musical position of this quantization point - const double quant_musical_position - = last_signature_change - + next_quant_bar_number * (4.0 * signature.num / signature.denom); - - // Map this to a time value - const double ratio = (quant_musical_position - start_position_in_quarters) - / musical_tick_duration; - const int64_t dt = end_frame() - prev_frame(); - int64_t frame_offset = dt * ratio; - - if(frame_offset < dt) - { - result.emplace_back( - frame_offset, static_cast(std::round( - next_quant_bar_number / bars_per_quantization))); - } + const double musical_tick_duration + = end_position_in_quarters - start_position_in_quarters; + const bool rewinding = musical_tick_duration < 0.; - next_quant_bar_number += bars_per_quantization; - } - } - else + if(rate <= 0. || musical_tick_duration == 0.) { - // Shorter than bars - subdivisions of quarters - - // Special handling when bar boundary occurs within this tick - if(bar_at_start != bar_at_end) - { - // There's a bar boundary within this tick - // We need to check for quantifications both before and after the boundary - - // Check if there's a quantification exactly at the bar boundary - const double bar_boundary_position - = bar_at_end; // This is the musical position of the bar boundary - - // Calculate if this bar boundary position is a subdivision point - const double subdivisions_per_quarter = rate / 4.; - - // Check if the bar boundary aligns with our subdivision rate - // For quarters (rate=4), every quarter boundary is a quantification point - // For eighths (rate=8), every eighth boundary is a quantification point, etc. - const double bar_relative_position - = 0.0; // At a bar boundary, we're at position 0 within the new bar - const double subdivision_at_boundary - = bar_relative_position * subdivisions_per_quarter; - - // If the bar boundary is within our tick range, it's always index 0 - if(bar_boundary_position >= start_position_in_quarters - && bar_boundary_position <= end_position_in_quarters - && std::abs(subdivision_at_boundary - std::round(subdivision_at_boundary)) - < 0.0001) - { - // Calculate the frame position of this bar boundary - const double offset_in_quarters - = bar_boundary_position - start_position_in_quarters; - const double ratio = offset_in_quarters / musical_tick_duration; - int64_t frame_offset = std::round(ratio * frames); - - if(frame_offset >= 0 && frame_offset < frames) - { - result.emplace_back(frame_offset, 0); // Bar boundaries are always index 0 - } - else if(frame_offset == frames) - { - // Bar boundary is exactly at the end of the tick - use the last frame - result.emplace_back(frames - 1, 0); // Bar boundaries are always index 0 - } - } - } - - const double start_quarter = start_position_in_quarters - bar_at_start; - const double end_quarter = end_position_in_quarters - bar_at_start; + res.emplace_back(0, 0); + return res; + } - // How many subdivisions per quarter note - // rate = 4 -> 1 per quarter, rate = 8 -> 2 per quarter, rate = 16 -> 4 per quarter - const double subdivisions_per_quarter = rate / 4.; + const bool valid_sig = signature.num > 0 && signature.denom > 0; + const double quarters_in_bar + = valid_sig ? 4. * signature.num / signature.denom : 4.; + constexpr double eps = 1e-9; + + // A point falling exactly on the end of the tick belongs to the next one, + // so the interval is half-open at the end the tick heads towards. + const auto try_push = [&](double musical_position, int index) { + const double ratio + = (musical_position - start_position_in_quarters) / musical_tick_duration; + int f = int(std::floor(ratio * this->frames)); + if(f < 0) + f = 0; + if(f >= this->frames) + return false; + res.emplace_back(f, index); + return res.size() < 1024; + }; - // Calculate actual number of subdivisions that can occur in this time signature - // For example: 7/8 time has 7 eighth notes, so 4 quarter note positions (0,2,4,6) - const int base_units_per_bar = signature.num; // e.g., 7 eighth notes in 7/8 - const int base_unit_denom = signature.denom; // e.g., 8 (eighth notes) + const double lo = rewinding ? end_position_in_quarters : start_position_in_quarters; + const double hi = rewinding ? start_position_in_quarters : end_position_in_quarters; - // How many subdivisions actually fit in the bar - int subdivisions_per_bar; - if(rate >= base_unit_denom) - { - // Subdivision is smaller than or equal to base unit (e.g., 16th notes in 7/8) - subdivisions_per_bar - = base_units_per_bar * (static_cast(rate) / base_unit_denom); - } - else + if(rate <= 1.) + { + // A bar or longer: counted from the last signature change, and no bar + // line subdivides it. + const double unit = quarters_in_bar / rate; + if(!(unit > 0.)) + return res; + + const double origin = last_signature_change; + const double start = (start_position_in_quarters - origin) / unit; + const double end = (end_position_in_quarters - origin) / unit; + const int64_t first = rewinding ? int64_t(std::floor(start + eps)) + : int64_t(std::ceil(start - eps)); + + for(int64_t k = first; rewinding ? (k > end + eps) : (k < end - eps); + k += rewinding ? -1 : 1) { - // Subdivision is larger than base unit (e.g., quarter notes in 7/8) - // Quarter notes occur every 2 eighth notes, so positions: 0, 2, 4, 6... - const int subdivision_interval = base_unit_denom / static_cast(rate); - subdivisions_per_bar = (base_units_per_bar + subdivision_interval - 1) - / subdivision_interval; // Ceiling division + if(!try_push(k * unit + origin, int(k))) + break; } + return res; + } - // Find first subdivision at or after start - const double start_subdivision = start_quarter * subdivisions_per_quarter; - int current_subdivision_index = static_cast(std::floor(start_subdivision)); - - // Check if we start exactly on a subdivision - if(std::abs(start_subdivision - current_subdivision_index) < 0.0001) + // Shorter than a bar: a subdivision of the quarter note, counted from the + // bar it falls in, so the grid restarts at every bar line. + const double unit = 4. / rate; + if(!(unit > 0.)) + return res; + + boost::container::small_vector, 8> segments; + for_each_bar_segment(lo, hi, [&](double bar_line, double next_bar) { + if(segments.size() < 1024) + segments.emplace_back(bar_line, next_bar); + }); + + const auto walk_segment = [&](double bar_line, double next_bar) { + const int divs = int(std::ceil((next_bar - bar_line) / unit)) + 1; + if(!rewinding) { - // Calculate position within the current bar - const double absolute_quarter_pos - = start_position_in_quarters - last_signature_change; - const double quarters_per_bar_exact = 4.0 * signature.num / signature.denom; - const double position_in_current_bar - = std::fmod(absolute_quarter_pos, quarters_per_bar_exact); - - // Convert to subdivision position within bar and get index - const double subdivisions_in_current_bar - = position_in_current_bar * subdivisions_per_quarter; - - // Find which subdivision slot this represents within the bar's subdivision pattern - int metric_index; - if(rate >= base_unit_denom) - { - // For subdivisions smaller than the base unit (e.g., 16ths in 7/8) - metric_index = static_cast(std::round(subdivisions_in_current_bar)) - % subdivisions_per_bar; - } - else + for(int k = 0; k <= divs; k++) { - // For subdivisions larger than base unit (e.g., quarters in 7/8) - // Count how many of this subdivision type have occurred within the bar - const int subdivision_interval = base_unit_denom / static_cast(rate); - const double subdivision_position_in_bar - = position_in_current_bar * (base_unit_denom / 4.0); - metric_index = static_cast(std::floor( - subdivision_position_in_bar / subdivision_interval)) - % subdivisions_per_bar; + const double p = bar_line + k * unit; + if(p >= next_bar - eps || p > hi + eps) + return true; + if(p < lo) + continue; + if(!try_push(p, k)) + return false; } - - result.emplace_back(0, metric_index); - current_subdivision_index++; } else { - current_subdivision_index++; - } - - // Find all subdivisions in the tick - const double end_subdivision = end_quarter * subdivisions_per_quarter; - - while(current_subdivision_index < end_subdivision) - { - // Calculate the position in quarters for this subdivision - const double quarter_position - = current_subdivision_index / subdivisions_per_quarter; - - // Calculate the absolute musical position - const double absolute_musical_position = bar_at_start + quarter_position; - - // Calculate frame offset within this tick - const double offset_in_quarters - = absolute_musical_position - start_position_in_quarters; - const double ratio = offset_in_quarters / musical_tick_duration; - int64_t frame_offset = std::round(ratio * frames); - - if(frame_offset < frames) + for(int k = divs; k >= 0; k--) { - // Calculate position within the current bar - const double absolute_quarter_pos - = absolute_musical_position - last_signature_change; - const double quarters_per_bar_exact = 4.0 * signature.num / signature.denom; - const double position_in_current_bar - = std::fmod(absolute_quarter_pos, quarters_per_bar_exact); - - // Convert to subdivision position within bar and get index - const double subdivisions_in_current_bar - = position_in_current_bar * subdivisions_per_quarter; - - // Find which subdivision slot this represents within the bar's subdivision pattern - int metric_index; - if(rate >= base_unit_denom) - { - // For subdivisions smaller than the base unit (e.g., 16ths in 7/8) - metric_index = static_cast(std::round(subdivisions_in_current_bar)) - % subdivisions_per_bar; - } - else - { - // For subdivisions larger than base unit (e.g., quarters in 7/8) - // Count how many of this subdivision type have occurred within the bar - const int subdivision_interval = base_unit_denom / static_cast(rate); - const double subdivision_position_in_bar - = position_in_current_bar * (base_unit_denom / 4.0); - metric_index = static_cast(std::floor( - subdivision_position_in_bar / subdivision_interval)) - % subdivisions_per_bar; - } - - result.emplace_back(frame_offset, metric_index); + const double p = bar_line + k * unit; + if(p >= next_bar - eps || p > hi + eps) + continue; + if(p < lo) + return true; + if(!try_push(p, k)) + return false; } - - current_subdivision_index++; } - } - - return result; - } + return true; + }; - [[nodiscard]] std::optional - get_quantification_date_for_bars_or_longer(double rate) const noexcept - { - std::optional quantification_date; - const double bars_per_quantization = 1.0 / rate; - - // Convert positions to bar numbers from the last signature - const double start_bar_position - = (start_position_in_quarters - last_signature_change) - / (4.0 * signature.num / signature.denom); - const double end_bar_position = (end_position_in_quarters - last_signature_change) - / (4.0 * signature.num / signature.denom); - - // Check if we're exactly on a quantization point at the start - const double start_remainder = std::fmod(start_bar_position, bars_per_quantization); - if(std::abs(start_remainder) < 0.0001 && start_position_in_quarters >= 0) + if(!rewinding) { - quantification_date = prev_frame(); + for(const auto& s : segments) + if(!walk_segment(s.first, s.second)) + break; } else { - // Find the next quantization bar after start - const double start_quant_bar - = std::floor(start_bar_position / bars_per_quantization); - const double next_quant_bar_number = (start_quant_bar + 1) * bars_per_quantization; - - // Check if this quantization point falls within our tick (but NOT at the end) - if(next_quant_bar_number > start_bar_position - && next_quant_bar_number < end_bar_position) - { - // Calculate the musical position of this quantization point - const double quant_musical_position - = last_signature_change - + next_quant_bar_number * (4.0 * signature.num / signature.denom); - - // Map this to a time value - const double musical_tick_duration - = end_position_in_quarters - start_position_in_quarters; - const double ratio = (quant_musical_position - start_position_in_quarters) - / musical_tick_duration; - const int64_t dt = end_frame() - prev_frame(); - - int64_t potential_date = prev_frame() + dt * ratio; - - // Extra safety check: ensure we're not at the boundary - if(potential_date < end_frame()) - { - quantification_date = potential_date; - } - } - } - return quantification_date; - } - - [[nodiscard]] std::optional - get_quantification_date_for_shorter_than_bars(double rate) const noexcept - { - std::optional quantification_date; - // Quantize relative to quarter divisions - // TODO ! if there is a bar change, - // and no prior quantization date before that, we have to quantize to the - // bar change - const double start_quarter = (start_position_in_quarters - bar_at_start); - const double end_quarter = (end_position_in_quarters - bar_at_start); - - // duration of what we quantify in terms of quarters - const double musical_quant_dur = rate / 4.; - const double start_quant = std::floor(start_quarter * musical_quant_dur); - const double end_quant = std::floor(end_quarter * musical_quant_dur); - - if(start_quant != end_quant) - { - // We want quantization on start, not on end - if(end_quant != end_quarter * musical_quant_dur) - { - // Date to quantify is the next one : - const double musical_tick_duration - = end_position_in_quarters - start_position_in_quarters; - const double quantified_duration = (bar_at_start + (start_quant + 1) * 4. / rate) - - start_position_in_quarters; - const double ratio = (end_frame() - prev_frame()) / musical_tick_duration; - - quantification_date = prev_frame() + quantified_duration * ratio; - } - } - else if(start_quant == start_quarter * musical_quant_dur) - { - // We start on a signature change - quantification_date = prev_frame(); + for(auto it = segments.rbegin(); it != segments.rend(); ++it) + if(!walk_segment(it->first, it->second)) + break; } - return quantification_date; + return res; } - //! Given a quantification rate (1 for bars, 2 for half, 4 for quarters...) - //! return the next occurring quantification date, if such date is in the tick - //! defined by this token_request. + //! The first quantification point of the tick, if any. + //! + //! This is the first of get_quantification_date(), not a second + //! implementation of it: a node that takes one point and a node that takes + //! them all have to agree about where the grid is. [[nodiscard]] std::optional get_one_quantification_date(double rate) const noexcept { if(prev_frame() == end_frame()) return std::nullopt; - if(rate <= 0.) - return prev_frame(); - - const double musical_tick_duration - = end_position_in_quarters - start_position_in_quarters; - if(musical_tick_duration <= 0.) - return prev_frame(); + // Quantized triggers are not interactive while rewinding. + if(end_position_in_quarters < start_position_in_quarters) + return std::nullopt; - if(rate <= 1.) - { - return get_quantification_date_for_bars_or_longer(rate); - } - else - { - return get_quantification_date_for_shorter_than_bars(rate); - } + const auto pts = get_quantification_date(rate); + if(pts.empty()) + return std::nullopt; + return int64_t(pts[0].first); } - // Given a quantification rate (1 for bars, 2 for half, 4 for quarters...) - // return the next occurring quantification date, if such date is in the tick - // defined by this token_request: div == 1 means 1 bar. - // Unlike the function above, this one also takes into account parent bar and time signature changes which - // may or may not be desired depending on the situation. - // FIXME does not seem to work either. + //! Like get_quantification_date, but each point says whether it is a bar line + //! rather than which subdivision it is. [[nodiscard]] halp::quantification_frames get_quantification_date_with_bars(double rate) const noexcept { - halp::quantification_frames quantification_date; - + halp::quantification_frames res = get_quantification_date(rate); if(rate <= 0.) - return {}; //fixme: tk.prev_date; + return res; - const double musical_tick_duration - = this->end_position_in_quarters - this->start_position_in_quarters; - if(musical_tick_duration <= 0.) - return {}; //fixme: this->prev_date; + // For a bar or longer every point is a bar line by construction; shorter + // than that, index 0 is the one sitting on the bar line. + for(auto& pt : res) + pt.second = (rate <= 1. || pt.second == 0) ? 1 : 0; + return res; + } - if(rate <= 1.) - { - // Quantize relative to bars - if(this->bar_at_end != this->bar_at_start) - { - // 4 if we're in 4/4 for instance - const double musical_bar_duration = this->bar_at_end - this->bar_at_start; + //! Reports every grid point the tick crosses, in order: bar lines through + //! tick(), the quarters between them through tock(). The same grid the + //! quantification dates use, so a click and a quantized event at one bar line + //! land on the same frame. + template + void metronome(Tick tick, Tock tock) const noexcept + { + const double musical_tick_duration + = end_position_in_quarters - start_position_in_quarters; + if(musical_tick_duration == 0. || this->frames <= 0) + return; + + const bool rewinding = musical_tick_duration < 0.; + const double lo = rewinding ? end_position_in_quarters : start_position_in_quarters; + const double hi = rewinding ? start_position_in_quarters : end_position_in_quarters; + + const auto frame_of = [&](double musical_position) { + const double ratio + = (musical_position - start_position_in_quarters) / musical_tick_duration; + int64_t f = int64_t(std::floor(ratio * this->frames)); + if(f < 0) + f = 0; + if(f >= this->frames) + f = this->frames - 1; + return f; + }; - // rate = 0.5 -> 2 bars at 3/4 -> 6 quarter notes - const double quantif_duration = musical_bar_duration / rate; + // A point sitting exactly on a tick boundary belongs to the tick that + // starts on it, where it is frame 0, not to the one that ends on it. + const auto emit = [&](double p, bool is_bar) { + if(rewinding ? (p > hi || p <= lo) : (p < lo || p >= hi)) + return; + if(is_bar) + tick(frame_of(p)); + else + tock(frame_of(p)); + }; - // we must be on quarter note 6, 12, 18, ... from the previous - // signature - const double rem = std::fmod( - this->bar_at_end - this->last_signature_change, quantif_duration); - if(rem < 0.0001) - { - // There is a bar change in this tick and it is when we are going to - // trigger - const double musical_bar_start - = this->bar_at_end - this->start_position_in_quarters; - - const double ratio = musical_bar_start / musical_tick_duration; - const int dt = this->frames; // TODO should be tick_offset - - // FIXME we should go "back" by as many measures length if length(bar) < length(tick) - // as right now we only catch the last bar change, but there may be multiple bar changes, - // with very fast tempos and long buffer sizes - quantification_date.push_back({std::floor(dt * ratio), 1}); - } - } - else if(start_position_in_quarters == 0.0) + double seg_lo[64]{}; + double seg_hi[64]{}; + int n_seg = 0; + for_each_bar_segment(lo, hi, [&](double bar_line, double next_bar) { + if(n_seg < 64) { - // Special first bar case - return {{0, 1}}; + seg_lo[n_seg] = bar_line; + seg_hi[n_seg] = next_bar; + n_seg++; } - } - else - { - // Quantize relative to quarter divisions - // TODO ! if there is a bar change, - // and no prior quantization date before that, we have to quantize to the - // bar change. To be handled by the host by splitting the buffer there. - const double start_quarter - = (this->start_position_in_quarters - this->bar_at_start); - const double end_quarter = (this->end_position_in_quarters - this->bar_at_start); - - // duration of what we quantify in terms of quarters - const double musical_quant_dur = rate / 4.; - const double start_quant = std::floor(start_quarter * musical_quant_dur); - const double end_quant = std::floor(end_quarter * musical_quant_dur); - - if(start_quant != end_quant) - { - if(end_quant == end_quarter * musical_quant_dur) - { - // We want quantization on start, not on end - return {}; - } - // Date to quantify is the next one : - const double musical_tick_duration - = this->end_position_in_quarters - this->start_position_in_quarters; - const double ratio = this->frames / musical_tick_duration; + }); - int i = 1; - for(;;) - { - const double quantified_duration - = (this->bar_at_start + (start_quant + i) * 4. / rate) - - this->start_position_in_quarters; - - if(int frame = std::floor(quantified_duration * ratio); frame < this->frames) - { - quantification_date.push_back({frame, 1}); - i++; - } - else - { - break; - } - } - } - else if(start_quant == start_quarter * musical_quant_dur) + const auto walk_segment = [&](double bar_line, double next_bar) { + if(!rewinding) { - // Special first bar case - return {{0, 1}}; + emit(bar_line, true); + for(double q = bar_line + 1.; q < next_bar - 1e-9; q += 1.) + emit(q, false); } - } - - return quantification_date; - } - - template - constexpr void metronome(Tick tick, Tock tock) const noexcept - { - if((bar_at_end != bar_at_start) || start_position_in_quarters == 0.) - { - // There is a bar change in this tick, start the up tick - const double musical_tick_duration - = end_position_in_quarters - start_position_in_quarters; - if(musical_tick_duration != 0) + else { - const double musical_bar_start = bar_at_end - start_position_in_quarters; - if(this->frames > 0) - { - const double ratio = musical_bar_start / musical_tick_duration; - const int64_t hi_start_sample = this->frames * ratio; - tick(hi_start_sample); - } + double last = bar_line; + for(double q = bar_line + 1.; q < next_bar - 1e-9; q += 1.) + last = q; + for(double q = last; q > bar_line + 1e-9; q -= 1.) + emit(q, false); + emit(bar_line, true); } - } + }; + + if(!rewinding) + for(int i = 0; i < n_seg; i++) + walk_segment(seg_lo[i], seg_hi[i]); else - { - const int64_t start_quarter - = std::floor(start_position_in_quarters - bar_at_start); - const int64_t end_quarter = std::floor(end_position_in_quarters - bar_at_start); - if(start_quarter != end_quarter) - { - // There is a quarter change in this tick, start the down tick - // start_position is prev_date - // end_position is date - const double musical_tick_duration - = end_position_in_quarters - start_position_in_quarters; - if(musical_tick_duration != 0) - { - const double musical_bar_start - = (end_quarter + bar_at_start) - start_position_in_quarters; - if(this->frames > 0) - { - const double ratio = musical_bar_start / musical_tick_duration; - const int64_t lo_start_sample = this->frames * ratio; - tock(lo_start_sample); - } - } - } - } + for(int i = n_seg - 1; i >= 0; i--) + walk_segment(seg_lo[i], seg_hi[i]); } // FIXME dpes that work for a bar change at frame 0 or 1 ? diff --git a/tests/quantification.cpp b/tests/quantification.cpp new file mode 100644 index 000000000..40e1e69d2 --- /dev/null +++ b/tests/quantification.cpp @@ -0,0 +1,193 @@ +#include + +#include + +namespace +{ +halp::tick_musical +musical_tick(int frames, double start_quarters, double end_quarters, double bar = 0.) +{ + halp::tick_musical t; + t.frames = frames; + t.tempo = 120.; + t.signature = {4, 4}; + t.position_in_frames = 0; + t.start_position_in_quarters = start_quarters; + t.end_position_in_quarters = end_quarters; + t.last_signature_change = 0.; + t.bar_at_start = bar; + t.bar_at_end = bar; + return t; +} +} + +TEST_CASE("quantification: forward sixteenths", "[audio][quantification]") +{ + // One quarter note across 64 frames, sixteenth-note rate: four points. + const auto t = musical_tick(64, 0., 1.); + const auto r = t.get_quantification_date(16.); + + REQUIRE(r.size() == 4); + CHECK(r[0] == std::pair{0, 0}); + CHECK(r[1] == std::pair{16, 1}); + CHECK(r[2] == std::pair{32, 2}); + CHECK(r[3] == std::pair{48, 3}); +} + +TEST_CASE("quantification: rewinding walks the same points backwards", + "[audio][quantification]") +{ + // The mirror of the tick above. Four subdivisions are crossed, in the + // opposite musical order, and their buffer positions still increase. + // + // A tick reports the point on its own start and leaves the one on its end to + // whatever runs next: forward that is [start; end[, rewinding ]end; start]. + // So this one reports quarter 1, where it begins, and leaves quarter 0 out. + const auto t = musical_tick(64, 1., 0.); + const auto r = t.get_quantification_date(16.); + + REQUIRE(r.size() == 4); + CHECK(r[0] == std::pair{0, 4}); + CHECK(r[1] == std::pair{16, 3}); + CHECK(r[2] == std::pair{32, 2}); + CHECK(r[3] == std::pair{48, 1}); + + int prev = -1; + for(auto [frame, index] : r) + { + CHECK(frame > prev); + CHECK(frame >= 0); + CHECK(frame < t.frames); + prev = frame; + } +} + +TEST_CASE("quantification: a rewinding tick shorter than the rate is silent", + "[audio][quantification]") +{ + // Used to report one point at frame 0 on every buffer, whatever the rate: + // a step sequencer driven by this fired once per buffer while rewinding. + const auto t = musical_tick(64, 0.95, 0.85); + CHECK(t.get_quantification_date(4.).empty()); + + const auto fwd = musical_tick(64, 0.85, 0.95); + CHECK(fwd.get_quantification_date(4.).empty()); +} + +TEST_CASE("quantification: rewinding over a bar", "[audio][quantification]") +{ + // Four quarters backwards over a whole bar at the quarter rate. + auto t = musical_tick(64, 4., 0.); + t.bar_at_start = 4.; + t.bar_at_end = 0.; + + const auto r = t.get_quantification_date(4.); + + REQUIRE(!r.empty()); + int prev = -1; + for(auto [frame, index] : r) + { + CHECK(frame > prev); + CHECK(frame >= 0); + CHECK(frame < t.frames); + prev = frame; + } +} + +TEST_CASE("quantification: points come out in buffer order", "[audio][quantification]") +{ + // A bar boundary inside the tick is appended to the result after the + // subdivisions that precede it in the buffer. Consumers walk the list in + // order - several stop at the first entry - so it has to be sorted. + // A whole bar, ending exactly on the next bar line: the boundary lands on + // the last frame but is appended before the four quarters that precede it. + auto t = musical_tick(64, 0., 4.); + t.bar_at_start = 0.; + t.bar_at_end = 4.; + + const auto r = t.get_quantification_date(4.); + + REQUIRE(r.size() >= 2); + int prev = -1; + for(auto [frame, index] : r) + { + CHECK(frame >= prev); + CHECK(frame >= 0); + CHECK(frame < t.frames); + prev = frame; + } +} + +TEST_CASE("quantification: degenerate ticks", "[audio][quantification]") +{ + // No musical duration: the single point at frame 0 is what objects driven + // with the musical fields left at zero rely on. + const auto flat = musical_tick(64, 1., 1.); + const auto r = flat.get_quantification_date(16.); + REQUIRE(r.size() == 1); + CHECK(r[0] == std::pair{0, 0}); + + // No frames at all. + const auto empty = musical_tick(0, 0., 1.); + CHECK(empty.get_quantification_date(16.).empty()); + + // A rate of zero means "every tick". + const auto zero_rate = musical_tick(64, 1., 0.); + const auto zr = zero_rate.get_quantification_date(0.); + REQUIRE(zr.size() == 1); + CHECK(zr[0] == std::pair{0, 0}); +} + +TEST_CASE("quantification: every reported frame is inside the buffer", + "[audio][quantification]") +{ + for(double rate : {1., 2., 4., 8., 16., 32.}) + { + for(int frames : {1, 7, 64, 512}) + { + for(double span : {0.1, 1., 4., 17.}) + { + const auto fwd = musical_tick(frames, 0., span); + const auto bwd = musical_tick(frames, span, 0.); + CAPTURE(rate, frames, span); + + for(auto [frame, index] : fwd.get_quantification_date(rate)) + { + CHECK(frame >= 0); + CHECK(frame < frames); + } + for(auto [frame, index] : bwd.get_quantification_date(rate)) + { + CHECK(frame >= 0); + CHECK(frame < frames); + } + } + } + } +} + +TEST_CASE("quantification: pos_to_frame stays inside the buffer", + "[audio][quantification]") +{ + const auto fwd = musical_tick(64, 0., 1.); + CHECK(fwd.pos_to_frame(0.) == 0); + CHECK(fwd.pos_to_frame(0.5) == 32); + + // Rewinding, the musical position moves down as the buffer advances. + const auto bwd = musical_tick(64, 1., 0.); + CHECK(bwd.pos_to_frame(1.) == 0); + CHECK(bwd.pos_to_frame(0.5) == 32); + + // A tick with no musical duration used to divide by zero here and convert a + // NaN to int. + const auto flat = musical_tick(64, 1., 1.); + CHECK(flat.pos_to_frame(1.) == 0); + CHECK(flat.pos_to_frame(9999.) == 0); + + // Out-of-tick positions clamp into the buffer rather than indexing past it. + CHECK(fwd.pos_to_frame(-100.) == 0); + CHECK(fwd.pos_to_frame(100.) == 63); + + const auto no_frames = musical_tick(0, 0., 1.); + CHECK(no_frames.pos_to_frame(0.5) == 0); +}