Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/ossia/dataflow/token_request.hpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#pragma once
#include <ossia/detail/flicks.hpp>
#include <ossia/detail/math.hpp>
#include <ossia/detail/small_vector.hpp>
#include <ossia/editor/scenario/time_signature.hpp>
#include <ossia/editor/scenario/time_value.hpp>

#include <cassert>
#include <cmath>
#include <optional>

#if defined(_LIBCPP_CONSTEXPR_SINCE_CXX23) || defined(_GLIBCXX23_CONSTEXPR)
Expand All @@ -16,6 +18,19 @@ namespace ossia
{
using quarter_note = double;

//! One quantification point inside a tick: when it happens, and which
//! subdivision it is.
struct quantification_point
{
ossia::time_value date{};
int64_t index{};

friend bool
operator==(const quantification_point&, const quantification_point&) noexcept
= default;
};
using quantification_points = ossia::small_vector<quantification_point, 8>;

struct token_request
{
constexpr token_request() noexcept = default;
Expand Down Expand Up @@ -360,6 +375,79 @@ struct token_request
}
}

//! Every quantification date occurring in this tick, in order.
//!
//! get_quantification_date only reports the first one, which silently drops
//! steps whenever a tick spans more than one - a small division, a large
//! buffer or a fast tempo are enough. Nodes that must not miss a step
//! iterate this instead. `index` counts the point from the last bar, or from
//! the last signature change for rates of a bar or longer.
[[nodiscard]] quantification_points
get_quantification_dates(double rate) const noexcept
{
quantification_points res;

if(prev_date == date)
return res;

const double musical_tick_duration = musical_end_position - musical_start_position;
if(rate <= 0. || musical_tick_duration <= 0.)
{
res.push_back({prev_date, 0});
return res;
}

// Distance in quarter notes between two consecutive points, and the musical
// position their count is relative to.
double unit{};
double origin{};
if(rate <= 1.)
{
// A bar or longer: the rate is a fraction of a bar.
const bool valid_sig = signature.upper > 0 && signature.lower > 0;
const double bar = valid_sig ? 4. * signature.upper / signature.lower : 4.;
unit = bar / rate;
origin = musical_start_last_signature;
}
else
{
// Shorter: a subdivision of the quarter note.
unit = 4. / rate;
origin = musical_start_last_bar;
}

if(!(unit > 0.))
return res;

// A point falling exactly on the end of the tick belongs to the next one,
// so the interval is [start; end[ - which is also what makes the first
// element agree with get_quantification_date().
constexpr double eps = 1e-9;
const double start = (musical_start_position - origin) / unit;
const double end = (musical_end_position - origin) / unit;

const time_value tick_duration = date - prev_date;
for(int64_t k = int64_t(std::ceil(start - eps)); k < end - eps; k++)
{
const double ratio
= (k * unit + origin - musical_start_position) / musical_tick_duration;
time_value d = prev_date + tick_duration * ratio;

if(d < prev_date)
d = prev_date;
if(d >= date)
break;

res.push_back({d, k});

// A tick spanning this many points means the rate is nonsense: stop
// rather than fill memory.
if(res.size() >= 1024)
break;
}
return res;
}

//! Like physical_quantification_date, but returns a date mapped to this tick
[[nodiscard]] ossia_constexpr_msvc_workaround std::optional<physical_time>
get_physical_quantification_date(double rate, double modelToSamples) const noexcept
Expand Down
130 changes: 130 additions & 0 deletions tests/Dataflow/TokenRequestTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include "include_catch.hpp"

#include <iterator>
#include <utility>
#include <vector>
TEST_CASE("test_loops_integer", "test_loops_integer")
{
using namespace ossia;
Expand Down Expand Up @@ -131,3 +133,131 @@ TEST_CASE("test_musical_quantization", "test_musical_quantization")
auto res = r.get_quantification_date(8.0);
REQUIRE(res == 0_tv);
}

namespace
{
using date_list = std::vector<std::pair<int64_t, int64_t>>;

//! A tick covering [prev; date[ in model time and [mstart; mend[ in quarters.
ossia::token_request musical_tick(
int64_t prev, int64_t date, double mstart, double mend, uint16_t num = 4, uint16_t denom = 4)
{
ossia::token_request t;
t.prev_date = ossia::time_value{prev};
t.date = ossia::time_value{date};
t.musical_start_position = mstart;
t.musical_end_position = mend;
t.musical_start_last_bar = 0.;
t.musical_start_last_signature = 0.;
t.signature = ossia::time_signature{num, denom};
return t;
}

date_list dates(const ossia::quantification_points& p)
{
date_list out;
for(auto& q : p)
out.emplace_back(q.date.impl, q.index);
return out;
}
}

TEST_CASE("quantification_dates_empty_tick", "quantification_dates")
{
auto t = musical_tick(100, 100, 0., 1.);
REQUIRE(t.get_quantification_dates(4).empty());
}

TEST_CASE("quantification_dates_no_rate", "quantification_dates")
{
// A null or negative rate means "as soon as possible".
auto t = musical_tick(100, 200, 0., 1.);
REQUIRE(dates(t.get_quantification_dates(0)) == date_list{{100, 0}});
REQUIRE(dates(t.get_quantification_dates(-1)) == date_list{{100, 0}});
}

TEST_CASE("quantification_dates_no_musical_info", "quantification_dates")
{
// Without a musical position there is nothing to quantize against: the tick
// itself is the point. Several nodes rely on this to step once per tick.
auto t = musical_tick(100, 200, 0., 0.);
REQUIRE(dates(t.get_quantification_dates(16)) == date_list{{100, 0}});
}

TEST_CASE("quantification_dates_several_per_tick", "quantification_dates")
{
// The whole point of the multi-date version: one tick worth a quarter note
// holds four sixteenths, and none of them may be dropped.
auto t = musical_tick(0, 1000, 0., 1.);

const auto expected = date_list{{0, 0}, {250, 1}, {500, 2}, {750, 3}};
REQUIRE(dates(t.get_quantification_dates(16)) == expected);
}

TEST_CASE("quantification_dates_interior_point", "quantification_dates")
{
auto t = musical_tick(0, 1000, 0.5, 1.5);
const auto res = t.get_quantification_dates(4);

REQUIRE(dates(res) == date_list{{500, 1}});
// and it agrees with the single-date version
auto single = t.get_quantification_date(4);
REQUIRE(single.has_value());
REQUIRE(*single == res[0].date);
}

TEST_CASE("quantification_dates_on_the_start_of_the_tick", "quantification_dates")
{
auto t = musical_tick(400, 1000, 1., 1.5);
const auto res = t.get_quantification_dates(4);

REQUIRE(dates(res) == date_list{{400, 1}});

auto single = t.get_quantification_date(4);
REQUIRE(single.has_value());
REQUIRE(*single == res[0].date);
}

TEST_CASE("quantification_dates_on_the_end_of_the_tick", "quantification_dates")
{
// A point falling exactly on the end of a tick belongs to the next one,
// otherwise it would fire twice.
auto t = musical_tick(0, 1000, 0.5, 1.);
REQUIRE(t.get_quantification_dates(4).empty());
REQUIRE(!t.get_quantification_date(4).has_value());
}

TEST_CASE("quantification_dates_bars", "quantification_dates")
{
// rate <= 1 counts bars: 1 is every bar, 0.5 every two bars.
auto t = musical_tick(0, 1000, 0., 8.);

REQUIRE(dates(t.get_quantification_dates(1)) == date_list{{0, 0}, {500, 1}});
REQUIRE(dates(t.get_quantification_dates(0.5)) == date_list{{0, 0}});
}

TEST_CASE("quantification_dates_odd_signature", "quantification_dates")
{
// 7/8: a bar is 3.5 quarters.
auto t = musical_tick(0, 1000, 0., 7., 7, 8);
REQUIRE(dates(t.get_quantification_dates(1)) == date_list{{0, 0}, {500, 1}});
}

TEST_CASE("quantification_dates_are_ordered_and_inside_the_tick", "quantification_dates")
{
for(double rate : {1., 2., 4., 8., 16., 32.})
{
auto t = musical_tick(0, 4096, 0.3, 5.7);
const auto res = t.get_quantification_dates(rate);

INFO("rate " << rate);
int64_t previous = -1;
for(const auto& q : res)
{
CHECK(q.date.impl >= t.prev_date.impl);
CHECK(q.date.impl < t.date.impl);
CHECK(q.date.impl >= previous);
previous = q.date.impl;
}
}
}
Loading