65f6070348
Hold keeps its picker. Also: one home for the %-fold, duration-ordered Hold travel, and a corrupt tail degrades to absent rather than fabricating one.
184 lines
7.5 KiB
C++
184 lines
7.5 KiB
C++
// note_program — the programmed capture signal: one note length, one velocity, two anchored
|
|
// offsets, and the one resolver a preview and a bake must share.
|
|
//
|
|
// Resolved times are rate-free seconds (this directory's CLAUDE.md: the standing ruling).
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <type_traits>
|
|
|
|
#include "core/instrument/note/musical_division.h"
|
|
#include "core/instrument/note/tempo.h"
|
|
|
|
namespace reasampler::instrument::note {
|
|
|
|
// The ladder and the offsets both feed the tempo conversions, so both must sit inside the
|
|
// domain fromBpm validates — checked here because this is the one file that composes them.
|
|
static_assert(kMaxDivisionBeats <= kMaxConvertibleMagnitude,
|
|
"the note-length ladder must stay inside the tempo conversions' domain");
|
|
|
|
class Velocity {
|
|
public:
|
|
static constexpr int kMin = 1; // 0 is note-off in MIDI; a programmed note must sound
|
|
static constexpr int kMax = 127;
|
|
|
|
Velocity() = default;
|
|
static Velocity of(int value); // clamped into [kMin, kMax]
|
|
|
|
constexpr std::uint8_t value() const { return value_; }
|
|
|
|
private:
|
|
std::uint8_t value_ = 100;
|
|
};
|
|
|
|
bool operator==(Velocity a, Velocity b);
|
|
|
|
enum class Denomination : std::uint8_t { Milliseconds, Beats };
|
|
|
|
class OffsetAmount;
|
|
|
|
// The one door. Normalizes both fields so nothing downstream has to: a magnitude past
|
|
// +/-kMaxConvertibleMagnitude clamps to it, a NaN magnitude — which names no value to clamp
|
|
// toward — becomes zero, and a denomination outside the enum becomes Milliseconds, the
|
|
// field's own default. A corrupt persisted record therefore resolves to a plausible offset
|
|
// rather than an unrepresentable one, and no two readers can disagree about which.
|
|
OffsetAmount offsetOf(double magnitude, Denomination denomination);
|
|
OffsetAmount offsetFromMs(double ms);
|
|
OffsetAmount offsetFromBeats(double beats);
|
|
|
|
// One magnitude, in the denomination it was entered in; the other view is derived on demand
|
|
// and never stored. Which one was entered is itself the intent: a beats offset must follow a
|
|
// tempo change and a ms offset must hold still, and only a stored denomination says which.
|
|
class OffsetAmount {
|
|
public:
|
|
OffsetAmount() = default;
|
|
|
|
constexpr double magnitude() const { return magnitude_; }
|
|
constexpr Denomination denomination() const { return denomination_; }
|
|
|
|
private:
|
|
OffsetAmount(double magnitude, Denomination denomination)
|
|
: magnitude_(magnitude), denomination_(denomination) {}
|
|
friend OffsetAmount offsetOf(double magnitude, Denomination denomination);
|
|
|
|
double magnitude_ = 0.0;
|
|
Denomination denomination_ = Denomination::Milliseconds;
|
|
};
|
|
|
|
static_assert(!std::is_constructible_v<OffsetAmount, double, Denomination>,
|
|
"offsetOf must be the only way to give an OffsetAmount a value");
|
|
|
|
bool operator==(OffsetAmount a, OffsetAmount b);
|
|
bool operator!=(OffsetAmount a, OffsetAmount b);
|
|
|
|
double offsetMs(OffsetAmount amount, Tempo tempo);
|
|
double offsetBeats(OffsetAmount amount, Tempo tempo);
|
|
double offsetSeconds(OffsetAmount amount, Tempo tempo);
|
|
|
|
// The unit toggle: the same instant restated in the other denomination.
|
|
OffsetAmount redenominate(OffsetAmount amount, Denomination to, Tempo tempo);
|
|
|
|
// Edit the magnitude via its non-stored view without changing which denomination is stored
|
|
// — a popup's ms and beats fields both stay live no matter which one the offset was entered
|
|
// in; only `redenominate` changes the stored denomination itself.
|
|
OffsetAmount withMsView(OffsetAmount amount, double ms, Tempo tempo);
|
|
OffsetAmount withBeatsView(OffsetAmount amount, double beats, Tempo tempo);
|
|
|
|
// The largest exact note length, in seconds — the ms ceiling restated in the unit an exact
|
|
// length is entered in, so a length and an offset cap at the same instant.
|
|
inline constexpr double kMaxLengthSeconds = msToSeconds(kMaxConvertibleMagnitude);
|
|
|
|
class NoteLength;
|
|
|
|
// The two doors. A DERIVED length is exact: `lengthOfSeconds` is what every computed duration
|
|
// takes, and rounding one onto the ladder is what truncates a source longer than the ladder's
|
|
// top rung. A PICKED length is a `Division`, because a picker's rungs are the point. Both
|
|
// clamp: a negative or NaN duration names no length and becomes zero, a magnitude past
|
|
// kMaxLengthSeconds clamps to it, and `makeDivision` already holds the rung's own domain.
|
|
NoteLength lengthOfDivision(Division division);
|
|
NoteLength lengthOfSeconds(double seconds);
|
|
|
|
// One length, in whichever denomination its door established. There is no accessor per
|
|
// denomination: `noteLengthSeconds` resolves both, so no reader can pick the wrong one.
|
|
class NoteLength {
|
|
public:
|
|
NoteLength() = default; // a quarter note — Division's own default
|
|
|
|
private:
|
|
NoteLength(Division division, double seconds, bool exact)
|
|
: division_(division), seconds_(seconds), exact_(exact) {}
|
|
friend NoteLength lengthOfDivision(Division division);
|
|
friend NoteLength lengthOfSeconds(double seconds);
|
|
friend double noteLengthSeconds(NoteLength length, Tempo tempo);
|
|
friend bool operator==(NoteLength a, NoteLength b);
|
|
|
|
Division division_{};
|
|
double seconds_ = 0.0;
|
|
bool exact_ = false;
|
|
};
|
|
|
|
static_assert(!std::is_constructible_v<NoteLength, Division>,
|
|
"lengthOfDivision must be the only way to give a NoteLength a rung");
|
|
|
|
bool operator==(NoteLength a, NoteLength b);
|
|
bool operator!=(NoteLength a, NoteLength b);
|
|
|
|
double noteLengthSeconds(NoteLength length, Tempo tempo);
|
|
|
|
// Two types rather than one carrying an anchor field: the anchor is then unswappable at
|
|
// compile time. Sign is uniform — positive is later in time — so a capture that opens before
|
|
// the note is a negative start offset, and a negative end offset truncates before release.
|
|
class StartOffset {
|
|
public:
|
|
StartOffset() = default;
|
|
explicit StartOffset(OffsetAmount amount) : amount_(amount) {}
|
|
OffsetAmount amount() const { return amount_; }
|
|
|
|
private:
|
|
OffsetAmount amount_{};
|
|
};
|
|
|
|
class EndOffset {
|
|
public:
|
|
EndOffset() = default;
|
|
explicit EndOffset(OffsetAmount amount) : amount_(amount) {}
|
|
OffsetAmount amount() const { return amount_; }
|
|
|
|
private:
|
|
OffsetAmount amount_{};
|
|
};
|
|
|
|
static_assert(!std::is_constructible_v<StartOffset, EndOffset>,
|
|
"StartOffset and EndOffset must not be interchangeable at compile time");
|
|
static_assert(!std::is_convertible_v<OffsetAmount, StartOffset>,
|
|
"the anchor constructor must stay explicit");
|
|
|
|
struct NoteProgram {
|
|
NoteLength length{};
|
|
StartOffset start{};
|
|
EndOffset end{};
|
|
Velocity velocity{};
|
|
};
|
|
|
|
bool operator==(const NoteProgram& a, const NoteProgram& b);
|
|
bool operator!=(const NoteProgram& a, const NoteProgram& b);
|
|
|
|
struct ResolvedNote {
|
|
double noteOffSeconds = 0.0; // == the note's sounding length, note-on being 0
|
|
double captureStartSeconds = 0.0; // negative when the capture opens before the note
|
|
double captureEndSeconds = 0.0;
|
|
std::uint8_t velocity = Velocity{}.value(); // resolveNote always overwrites this
|
|
// True when the programmed end offset inverted the window and resolveNote collapsed it
|
|
// to zero length instead — lets a popup explain an empty window rather than just show one.
|
|
bool windowCollapsed = false;
|
|
|
|
double captureLengthSeconds() const { return captureEndSeconds - captureStartSeconds; }
|
|
};
|
|
|
|
// Total: every field of the result is finite for every constructible program and tempo,
|
|
// which is why there is no failure path here. See this directory's CLAUDE.md.
|
|
ResolvedNote resolveNote(const NoteProgram& program, Tempo tempo);
|
|
|
|
} // namespace reasampler::instrument::note
|