Tabula Sonora 0.1.0
A native C++20 implementation of the Roland Sound Canvas VA synth voice
Loading...
Searching...
No Matches
ts::SegmentEnvelope Class Reference

A four-segment envelope with a release, evaluated at any sample position. More...

#include <tabulasonora/segment_envelope.hpp>

Public Member Functions

 SegmentEnvelope (const EnvelopeMachine &machine, std::span< const double > targets, std::span< const double > segment_samples, std::span< const bool > linear, double release_target, double release_samples, bool release_linear, double after_release, std::int64_t control_tick_samples)
 Builds an envelope from decoded segment parameters.
std::int64_t note_off_sample () const noexcept
 Where the release starts, or -1 while the note is still held.
std::int64_t release_samples () const noexcept
 The release duration in samples, at least one.
void note_off (std::int64_t sample, int damper=0)
 Starts the release at a sample position.
double value_at (std::int64_t sample) const noexcept
 The envelope's value at a sample position relative to note-on.
bool is_finished (std::int64_t sample) const noexcept
 Whether the envelope has reached silence and cannot leave it.
std::span< const double, segment_counttargets () const noexcept
 The gain each segment ramps to, in the order they run.
std::span< const std::int64_t, segment_countsegment_ends () const noexcept
 Where each segment ends, in samples from note-on.

Static Public Member Functions

static std::int64_t defer_to_control_tick (std::int64_t sample, std::int64_t control_tick_samples) noexcept
 The control tick an event landing at a sample is acted on.

Static Public Attributes

static constexpr int segment_count = 4
 Segments before the release.

Detailed Description

A four-segment envelope with a release, evaluated at any sample position.

The TVA and TVF envelopes have the same shape — four segments that run from note-on, a hold at the last target, then a release that starts wherever note-off caught it. What differs is only how the targets and durations are decoded, which is the job of TvaChain and TvfChain.

The trajectory is a pure function of the sample index and the note-off position, so the offline renderer and the real-time voice loop read the same envelope by construction rather than by agreement between two transcriptions. Nothing here is advanced by rendering: a block renderer evaluates it per sample, and an offline one fills an array.

Constructor & Destructor Documentation

◆ SegmentEnvelope()

ts::SegmentEnvelope::SegmentEnvelope ( const EnvelopeMachine & machine,
std::span< const double > targets,
std::span< const double > segment_samples,
std::span< const bool > linear,
double release_target,
double release_samples,
bool release_linear,
double after_release,
std::int64_t control_tick_samples )

Builds an envelope from decoded segment parameters.

control_tick_samples is the grid note-off is acted on. A tick of one releases at the note after note-off, which is as close to releasing immediately as this gets and is not what the engine does; see note_off.

Throws std::invalid_argument if a parameter array is not four long.

Member Function Documentation

◆ note_off_sample()

std::int64_t ts::SegmentEnvelope::note_off_sample ( ) const
inlinenodiscardnoexcept

Where the release starts, or -1 while the note is still held.

◆ release_samples()

std::int64_t ts::SegmentEnvelope::release_samples ( ) const
inlinenodiscardnoexcept

The release duration in samples, at least one.

◆ note_off()

void ts::SegmentEnvelope::note_off ( std::int64_t sample,
int damper = 0 )

Starts the release at a sample position.

Later calls are ignored.

The release departs from the value the envelope had reached, not from a segment target, which is what makes a note released mid-attack decay from where it actually was.

Note-off does not take effect at the sample it lands on: the engine sees it at its next control tick, so the envelope holds for the rest of the current tick first. Measured by sweeping the hold time past a tick boundary — note-off anywhere in 1000–1008 ms produced the same release, which then stepped a whole tick later at 1010 ms. Releasing immediately instead runs the tail 0–10 ms early; that is inaudible on a pad but a large fraction of a short release, and the Accordion reaches -20 dB in 9 ms against the engine's 23 ms.

damper is the CC64 value at release, 1–0x3f for a half-pressed pedal on a half-damper tone, else zero. The engine writes it into the release ramp's rate-scale byte, which multiplies the rate word by roughly 1 - v/128, so a half-pressed pedal lengthens the release by the reciprocal. Only the 57 piano tones carry the capability; every other tone's pedal value is quantised to 0 or 0x7f before it can get here.

◆ defer_to_control_tick()

std::int64_t ts::SegmentEnvelope::defer_to_control_tick ( std::int64_t sample,
std::int64_t control_tick_samples )
staticnodiscardnoexcept

The control tick an event landing at a sample is acted on.

The following tick, even when the event lands exactly on a boundary: that tick's update has already run by the time the event is latched. Measured on the DLL — a note-off at 1010 ms, exactly a tick, released at 1020 ms, while one at 1008 ms released at 1010 ms. So the deferral spans one full tick and is never zero.

◆ value_at()

double ts::SegmentEnvelope::value_at ( std::int64_t sample) const
nodiscardnoexcept

The envelope's value at a sample position relative to note-on.

◆ is_finished()

bool ts::SegmentEnvelope::is_finished ( std::int64_t sample) const
inlinenodiscardnoexcept

Whether the envelope has reached silence and cannot leave it.

Two ways to get there. The release running out is the usual one. The other is an envelope whose last segment ends at zero: it has nowhere left to go, and a voice that never receives a note-off — every drum key but a handful — would otherwise sit in the pool forever holding silence. The module frees those when the gain reaches zero, which is the same rule.

◆ targets()

std::span< const double, segment_count > ts::SegmentEnvelope::targets ( ) const
inlinenodiscardnoexcept

The gain each segment ramps to, in the order they run.

Exposed so a built envelope can be read back and compared against the module's own, which scdec tvatrace lifts out of voice+0x16/0x1d2/0x1d4/0x1d6 after note-on. Inferring an envelope error from the audio it produces is guesswork; this makes it arithmetic.

◆ segment_ends()

std::span< const std::int64_t, segment_count > ts::SegmentEnvelope::segment_ends ( ) const
inlinenodiscardnoexcept

Where each segment ends, in samples from note-on.

The module's counterpart is a duration in milliseconds at voice+0x12/0x1c6/0x1c8/0x1ca, so these are cumulative and those are not.

Member Data Documentation

◆ segment_count

int ts::SegmentEnvelope::segment_count = 4
staticconstexpr

Segments before the release.


The documentation for this class was generated from the following file: