note: land the programmed capture-signal model — division ladder, tempo resolution, anchored offsets, one record and one resolver

This commit is contained in:
2026-07-30 19:44:15 -04:00
parent 7bd911d58b
commit 834a6ddcc7
12 changed files with 1098 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
// 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 relative to note-on; the caller converts against the
// live sample rate.
#pragma once
#include <cstdint>
#include "core/instrument/note/musical_division.h"
#include "core/instrument/note/tempo.h"
namespace reasampler::instrument::note {
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]
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 };
// 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.
struct OffsetAmount {
double magnitude = 0.0;
Denomination denomination = Denomination::Milliseconds;
};
bool operator==(OffsetAmount a, OffsetAmount b);
bool operator!=(OffsetAmount a, OffsetAmount b);
OffsetAmount offsetFromMs(double ms);
OffsetAmount offsetFromBeats(double beats);
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);
// 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_{};
};
struct NoteProgram {
Division 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 = 1;
double captureLengthSeconds() const { return captureEndSeconds - captureStartSeconds; }
};
ResolvedNote resolveNote(const NoteProgram& program, Tempo tempo);
} // namespace reasampler::instrument::note