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

The segment-rate and segment-shape machine shared by the TVA, TVF and pitch envelopes. More...

#include <tabulasonora/envelope_machine.hpp>

Public Member Functions

 EnvelopeMachine (const TableSet &tables)
 Creates the machine over a loaded table set, which must outlive it.
int rate_scale (int base_rate, int modifier) const noexcept
 Converts a base rate byte and a 0x40-neutral modifier into an 8.8 rate multiplier.
int level_scale (int level, int modifier) const noexcept
 Converts a level byte and a 0x40-neutral modifier into an 8.8 multiplier.
double segment_milliseconds (int rate_byte, int rate_multiplier, int velocity_multiplier=unity_multiplier, int bias=0) const noexcept
 The duration of one envelope segment in milliseconds, or zero for an instant segment.
double segment_curve (double position, double start, double target, bool linear) const noexcept
 Interpolates one segment's value at a normalised position, 0 to 1.
std::int64_t hold_samples (const PartialParameters &partial, int velocity) const noexcept
 How long a partial's envelope machine stays held at its note-on state, in samples.

Static Public Member Functions

static constexpr bool is_linear_segment (int rate_byte) noexcept
 True when a segment interpolates linearly rather than through the fast-approach curve.

Static Public Attributes

static constexpr int unity_multiplier = 0x100
 Neutral value of an 8.8 multiplier, meaning 1.0.
static constexpr int minimum_segment_ticks = 9
 A rate-curve entry below this yields a zero-length segment.
static constexpr std::int64_t hold_forever = std::numeric_limits<std::int64_t>::max()
 A hold that never expires: the envelope machine stays at its note-on state.

Detailed Description

The segment-rate and segment-shape machine shared by the TVA, TVF and pitch envelopes.

An envelope is a chain of segments, each with a target level and a rate byte. The rate byte selects a base duration from g_rate_curve, which two 8.8 multipliers then scale: one from key-follow and one from velocity. A segment completes when a 16-bit phase accumulator wraps, which makes the duration 0x10000 / (rate * speed) * 10 ms at the 100 Hz control tick.

Everything here is integer arithmetic with deliberate 16-bit truncation. The control path of the original is exclusively 16-bit fixed point, and several of these expressions depend on wrapping or on truncation toward zero.

Constructor & Destructor Documentation

◆ EnvelopeMachine()

ts::EnvelopeMachine::EnvelopeMachine ( const TableSet & tables)
explicit

Creates the machine over a loaded table set, which must outlive it.

Member Function Documentation

◆ is_linear_segment()

constexpr bool ts::EnvelopeMachine::is_linear_segment ( int rate_byte)
inlinestaticnodiscardconstexprnoexcept

True when a segment interpolates linearly rather than through the fast-approach curve.

Bit 7 of the rate byte is the segment shape flag, not part of the rate. The TVF and pitch envelopes hardcode linear; the TVA is the only one where the shape is data-driven.

◆ rate_scale()

int ts::EnvelopeMachine::rate_scale ( int base_rate,
int modifier ) const
nodiscardnoexcept

Converts a base rate byte and a 0x40-neutral modifier into an 8.8 rate multiplier.

◆ level_scale()

int ts::EnvelopeMachine::level_scale ( int level,
int modifier ) const
nodiscardnoexcept

Converts a level byte and a 0x40-neutral modifier into an 8.8 multiplier.

◆ segment_milliseconds()

double ts::EnvelopeMachine::segment_milliseconds ( int rate_byte,
int rate_multiplier,
int velocity_multiplier = unity_multiplier,
int bias = 0 ) const
nodiscardnoexcept

The duration of one envelope segment in milliseconds, or zero for an instant segment.

rate_byte's bit 7 is the shape flag and is masked off. bias is a part-level rate offset, zero when both patch rate parameters are neutral.

◆ segment_curve()

double ts::EnvelopeMachine::segment_curve ( double position,
double start,
double target,
bool linear ) const
nodiscardnoexcept

Interpolates one segment's value at a normalised position, 0 to 1.

The fast-approach curve must be interpolated between adjacent entries using the low byte of the phase. A bare 256-level lookup steps across the segment, and on a long decay those steps are amplitude discontinuities that inject a broadband noise floor — roughly 80 dB of sustain signal-to-noise against the engine's 96 dB.

◆ hold_samples()

std::int64_t ts::EnvelopeMachine::hold_samples ( const PartialParameters & partial,
int velocity ) const
nodiscardnoexcept

How long a partial's envelope machine stays held at its note-on state, in samples.

Zero for a normal partial; a sample count for a delayed start; hold_forever for a one-shot.

Partial block byte 0x00 arms a one-shot clock at note-on. While it runs, none of the voice's envelopes or LFOs advance — every control value stays where the note-on compute left it, which for an ordinary attack envelope is silence. When it fires, the machine simply starts. It fires once; it is a delayed start, not a tremolo. 0xff is the one-shot form: held for the voice's whole life, so the sample plays at its note-on control levels (the .o variation tones), and note-off takes the fast fade instead of the release.

Member Data Documentation

◆ unity_multiplier

int ts::EnvelopeMachine::unity_multiplier = 0x100
staticconstexpr

Neutral value of an 8.8 multiplier, meaning 1.0.

◆ minimum_segment_ticks

int ts::EnvelopeMachine::minimum_segment_ticks = 9
staticconstexpr

A rate-curve entry below this yields a zero-length segment.

◆ hold_forever

std::int64_t ts::EnvelopeMachine::hold_forever = std::numeric_limits<std::int64_t>::max()
staticconstexpr

A hold that never expires: the envelope machine stays at its note-on state.


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