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

One of the sixteen parts: the channel state a running engine keeps between events. More...

#include <tabulasonora/part.hpp>

Public Member Functions

 Part ()
int key_pressure (int note) const noexcept
 The pressure on one key, or zero for anything out of range.
PartModifiers modifiers () const noexcept
 The seven live modify offsets, in the form the synthesis chains take.
int effective_velocity (int velocity) const noexcept
 The velocity a note-on actually sounds at, after this part's velocity sense (40 1x 1A depth and 40 1x 1B offset).
bool damper_down () const noexcept
 Whether the damper is holding notes on.
int volume () const noexcept
 CC#7 volume.
void set_volume (int value) noexcept
int expression () const noexcept
 CC#11 expression.
void set_expression (int value) noexcept
int master () const noexcept
 Master volume, which is global but folds into the same law.
void set_master (int value) noexcept
double volume_scale () const noexcept
 The combined volume multiplier, 1.0 with everything at 127.
ControlMatrix::Modulation matrix (int key_pressure=0) const noexcept
 Everything the control matrix modulates, each destination in its own unit.
double matrix_pitch_milli_semitones (int key_pressure=0) const noexcept
 The control matrix's contribution to pitch alone, in milli-semitones.
double bend_milli_semitones () const noexcept
 The bend offset in milli-semitones.
double tune_milli_semitones () const noexcept
 The part's static tune in milli-semitones: the net of RPN coarse and fine tune and the GS part key shift.
double scale_offset_milli_semitones (int key) const noexcept
 The scale-tuning offset for a key, in milli-semitones: a cent a step, ten milli-semitones a cent.
bool rpn_is_null () const noexcept
 Whether the RPN address currently selected is the null set (7F/7F), which parks data entry.
void reset ()
 Returns the part to its power-on state.
void reset_controllers ()
 Applies CC#121, which resets the controllers a reset message covers.

Public Attributes

int program = 0
 Program in force, from the last program change.
int bank = 0
 Bank select MSB, which carries the variation.
int bank_lsb = 0
 Bank select LSB, which on this module selects the tone map: 1-4 name a vintage and 0 keeps the configured default.
int rx_channel = 0
 The MIDI channel this part listens on, 0-15, or 16 for off.
RxSwitches rx
 The GS receive switches, written by SysEx 40 1x 03-12 and 23/24.
int pan = sequence_builder::default_pan
 CC#10 pan.
int modulation = 0
 CC#1 modulation.
int damper = 0
 CC#64 damper.
int reverb_send = sequence_builder::default_reverb_send
 CC#91 reverb send.
int chorus_send = sequence_builder::default_chorus_send
 CC#93 chorus send.
int delay_send = 0
 Part delay send, which has no Control Change and arrives only over SysEx.
int bend = 8192
 CC#94 delay send.
int bend_range = 2
 Bend range in semitones, from RPN 00/00.
int fine_tune = 0x2000
 RPN 00/01 fine tune, as the full 14-bit value with 0x2000 centred; +-100 cents at the ends.
int coarse_tune = 0x40
 RPN 00/02 coarse tune, 0x40 centred, one semitone a step.
int key_shift = 0x40
 GS part key shift (40 1x 16), 0x40 centred; the engine clamps it to 0x28-0x58.
ControlMatrix control
 The controller assignment matrix (40 2x), which decides what each source modulates.
int channel_pressure = 0
 Channel aftertouch, which reaches pitch through the control matrix.
int cc1_number = 16
 Which Control Change numbers this part's two assignable sources listen to (40 1x 1F and 20), and what those controllers currently read.
int cc2_number = 17
int cc1 = 0
int cc2 = 0
int xg_bank_msb = -1
 Bank select MSB while the engine is in XG mode, where it means something else entirely.
std::array< std::uint8_t, 128 > poly_pressure {}
 Polyphonic aftertouch, per key.
int vibrato_rate = 0x40
 The GS part modify offsets, 0x40 centred.
int vibrato_depth = 0x40
int vibrato_delay = 0x40
int tvf_cutoff = 0x40
int env_attack = 0x40
int env_decay = 0x40
int env_release = 0x40
int tvf_resonance = 0x40
 The eighth: CC#71 / NRPN 01 21 / 40 1x 33, and the one that goes nowhere.
std::array< int, 12 > scale_tuning
 GS scale tuning (40 1x 40-4B), one entry per pitch class, 0x40 centred, a cent a step.
int rhythm = -1
 GS use-for-rhythm (40 1x 15): -1 follows the channel default, 0 forces melodic, 1 or 2 route the part to the drum path on that drum map.
int key_low = 0
 GS keyboard range (40 1x 1D/1E); note-ons outside it are ignored on melodic parts.
int key_high = 0x7F
bool eq_enabled = false
 Whether this part runs through the four-band EQ (40 4x 20), which is part+0x450.
bool efx_enabled = false
 Whether this part feeds the insertion EFX block (40 4x 22), which is part+0x452.
int velocity_depth = 0x40
 GS velocity sense depth and offset (40 1x 1A/1B), applied by effective_velocity.
int velocity_offset = 0x40
int pitch_offset_fine = 0x08
 GS pitch offset fine (40 1x 17), stored raw: the unit is Hz on the module, which nothing downstream consumes yet.
int rpn_msb = 0x7F
int rpn_lsb = 0x7F
int nrpn_msb = 0x7F
int nrpn_lsb = 0x7F
 The selected NRPN's LSB, which for the drum parameters is the key number.
bool data_entry_is_nrpn = false
 Whether a CC#6 data entry commits to the selected NRPN rather than the selected RPN.
DrumKeyOverrides drum_keys
 Per-drum-key overrides this part has taken from NRPN.
std::vector< int > sustained
 Notes whose release is waiting for the damper to lift.
int portamento_time = 0
 CC#5 portamento time; indexes the glide-step table.
bool portamento_on = false
 Whether CC#65 portamento is on.
bool mono = false
 Whether CC#126 mono mode is on, which flushes the part's voices at each note-on.
bool soft = false
 Whether the CC#67 soft pedal is down.
int portamento_control_key = -1
 CC#84 portamento control: the key the next note glides from, or -1 when unset.
int last_key = -1
 The key the part last sounded, which portamento glides from, or -1.
bool sostenuto_down = false
 Whether the sostenuto pedal is down. CC#66 is binary — bit 6 only, as the engine reads it.
std::vector< int > sostenuto_captured
 Notes the sostenuto pedal captured — the ones sounding when it went down.
std::vector< int > sostenuto_released
 Captured notes whose note-off arrived while the pedal held them.

Detailed Description

One of the sixteen parts: the channel state a running engine keeps between events.

The engine has no MIDI output, so nothing can be read back from it — every piece of channel state has to be tracked here. This is the live counterpart of PartTimelines, which records the same values as breakpoints for the offline renderer.

Volume, expression and master are set through methods rather than left as public fields: each one recomputes the combined scale, and a plain assignment that skipped that would leave the part sounding at its previous level until the next one happened to move.

Constructor & Destructor Documentation

◆ Part()

ts::Part::Part ( )
inline

Member Function Documentation

◆ key_pressure()

int ts::Part::key_pressure ( int note) const
inlinenodiscardnoexcept

The pressure on one key, or zero for anything out of range.

◆ modifiers()

PartModifiers ts::Part::modifiers ( ) const
inlinenodiscardnoexcept

The seven live modify offsets, in the form the synthesis chains take.

◆ effective_velocity()

int ts::Part::effective_velocity ( int velocity) const
inlinenodiscardnoexcept

The velocity a note-on actually sounds at, after this part's velocity sense (40 1x 1A depth and 40 1x 1B offset).

Three details are load-bearing. A depth of zero collapses every velocity to 1, not to silence. A depth of exactly 0x40 skips the multiply rather than multiplying by one, so a neutral part cannot lose a count to the shift's truncation. And the low clamp is to 1 as well: an offset that drives the sum negative still sounds, at the floor.

◆ damper_down()

bool ts::Part::damper_down ( ) const
inlinenodiscardnoexcept

Whether the damper is holding notes on.

◆ volume()

int ts::Part::volume ( ) const
inlinenodiscardnoexcept

CC#7 volume.

◆ set_volume()

void ts::Part::set_volume ( int value)
inlinenoexcept

◆ expression()

int ts::Part::expression ( ) const
inlinenodiscardnoexcept

CC#11 expression.

◆ set_expression()

void ts::Part::set_expression ( int value)
inlinenoexcept

◆ master()

int ts::Part::master ( ) const
inlinenodiscardnoexcept

Master volume, which is global but folds into the same law.

◆ set_master()

void ts::Part::set_master ( int value)
inlinenoexcept

◆ volume_scale()

double ts::Part::volume_scale ( ) const
inlinenodiscardnoexcept

The combined volume multiplier, 1.0 with everything at 127.

◆ matrix()

ControlMatrix::Modulation ts::Part::matrix ( int key_pressure = 0) const
inlinenodiscardnoexcept

Everything the control matrix modulates, each destination in its own unit.

part_mod_depth_recalc keeps eleven running sums a part, one a destination, each the total of the sources' contributions, and clamps and scales each by its own law. This is that function, computed on demand rather than cached behind a dirty mask — the mask is a way of not recomputing, not a difference in what is computed.

All six sources reach it: the mod wheel, bend, both aftertouches, and the two assignable controllers. Every one is summed raw before anything is clamped, which is why they are gathered here rather than scaled separately and added up — clamping each in turn would let the total escape the rail.

key_pressure is the polyphonic aftertouch on the note being rendered, which is why this takes an argument at all: everything else here belongs to the part, and that one belongs to the key. The module applies it at exactly this point too, and for the same reason — it is the one source whose amount is not a property of the part, so it cannot be folded into a per-part cache. A caller with no note in hand passes nothing.

◆ matrix_pitch_milli_semitones()

double ts::Part::matrix_pitch_milli_semitones ( int key_pressure = 0) const
inlinenodiscardnoexcept

The control matrix's contribution to pitch alone, in milli-semitones.

The clamp behind this is not a round number by accident: 0xbe8 is 3048, which is 127 × 24, exactly the largest value amount × (depth − 0x40) can reach. The scale then works out to 24 semitones at the rail, which is what fixes the unit as milli-semitones.

◆ bend_milli_semitones()

double ts::Part::bend_milli_semitones ( ) const
inlinenodiscardnoexcept

The bend offset in milli-semitones.

Retained for callers that want bend alone — the offline timelines still build a curve from it. The voice no longer uses it: bend reaches pitch through the control matrix, which is where the engine puts it and which differs from this by up to 0.8 cents at full deflection.

◆ tune_milli_semitones()

double ts::Part::tune_milli_semitones ( ) const
inlinenodiscardnoexcept

The part's static tune in milli-semitones: the net of RPN coarse and fine tune and the GS part key shift.

One summed value, matching the engine, which folds all three into a single s16 milli-semitone offset (part+0x3ba) added to every voice's pitch.

◆ scale_offset_milli_semitones()

double ts::Part::scale_offset_milli_semitones ( int key) const
inlinenodiscardnoexcept

The scale-tuning offset for a key, in milli-semitones: a cent a step, ten milli-semitones a cent.

◆ rpn_is_null()

bool ts::Part::rpn_is_null ( ) const
inlinenodiscardnoexcept

Whether the RPN address currently selected is the null set (7F/7F), which parks data entry.

◆ reset()

void ts::Part::reset ( )

Returns the part to its power-on state.

◆ reset_controllers()

void ts::Part::reset_controllers ( )
inline

Applies CC#121, which resets the controllers a reset message covers.

The engine's caseD_79: modulation and pressure cleared, damper and soft and portamento lifted, expression back to full, the one-shot portamento key consumed, and the RPN/NRPN selection parked at null (rpn_nrpn_decode case 0x79). Volume, pan, the sends and the tuning stay – a reset is not a power cycle.

Releasing what the damper held is the caller's job, since it owns the voices.

Member Data Documentation

◆ program

int ts::Part::program = 0

Program in force, from the last program change.

◆ bank

int ts::Part::bank = 0

Bank select MSB, which carries the variation.

◆ bank_lsb

int ts::Part::bank_lsb = 0

Bank select LSB, which on this module selects the tone map: 1-4 name a vintage and 0 keeps the configured default.

The engine clamps the SysEx form to 1-4 (sysex_part_bank_lsb).

◆ rx_channel

int ts::Part::rx_channel = 0

The MIDI channel this part listens on, 0-15, or 16 for off.

GS SysEx 40 1x 02.

Owned by the tone generator rather than reset here, because the power-on value is the part's own channel and the part does not know its index.

◆ rx

RxSwitches ts::Part::rx

The GS receive switches, written by SysEx 40 1x 03-12 and 23/24.

◆ pan

int ts::Part::pan = sequence_builder::default_pan

CC#10 pan.

◆ modulation

int ts::Part::modulation = 0

CC#1 modulation.

◆ damper

int ts::Part::damper = 0

CC#64 damper.

◆ reverb_send

int ts::Part::reverb_send = sequence_builder::default_reverb_send

CC#91 reverb send.

◆ chorus_send

int ts::Part::chorus_send = sequence_builder::default_chorus_send

CC#93 chorus send.

◆ delay_send

int ts::Part::delay_send = 0

Part delay send, which has no Control Change and arrives only over SysEx.

◆ bend

int ts::Part::bend = 8192

CC#94 delay send.

The Control Change alias of the SysEx part delay send: the engine routes both into the same byte (caseD_5e writes part+0x44a, as does 40 1x 2C). Kept as the one delay_send field above. Pitch bend, as a 14-bit value with 8192 centred.

◆ bend_range

int ts::Part::bend_range = 2

Bend range in semitones, from RPN 00/00.

◆ fine_tune

int ts::Part::fine_tune = 0x2000

RPN 00/01 fine tune, as the full 14-bit value with 0x2000 centred; +-100 cents at the ends.

◆ coarse_tune

int ts::Part::coarse_tune = 0x40

RPN 00/02 coarse tune, 0x40 centred, one semitone a step.

◆ key_shift

int ts::Part::key_shift = 0x40

GS part key shift (40 1x 16), 0x40 centred; the engine clamps it to 0x28-0x58.

◆ control

ControlMatrix ts::Part::control

The controller assignment matrix (40 2x), which decides what each source modulates.

Bend's pitch depth is the one route that lives elsewhere: it is bend_range above, because the engine stores it in the same byte RPN 00/00 writes. See ControlMatrix.

◆ channel_pressure

int ts::Part::channel_pressure = 0

Channel aftertouch, which reaches pitch through the control matrix.

◆ cc1_number

int ts::Part::cc1_number = 16

Which Control Change numbers this part's two assignable sources listen to (40 1x 1F and 20), and what those controllers currently read.

The numbers default to 16 and 17 — General Purpose 1 and 2, which is what makes those two controllers do anything on a GS module at all. Nothing else in the engine reads CC#16 or CC#17; they exist to be pointed at the matrix.

An assignable source is not a controller with a fixed meaning, so it is tracked as a pair: the number decides which message feeds it, and the amount is what the matrix scales. Pointing CC1 at a controller that already means something — the mod wheel, say — does not take that meaning away; the message does both jobs.

◆ cc2_number

int ts::Part::cc2_number = 17

◆ cc1

int ts::Part::cc1 = 0

◆ cc2

int ts::Part::cc2 = 0

◆ xg_bank_msb

int ts::Part::xg_bank_msb = -1

Bank select MSB while the engine is in XG mode, where it means something else entirely.

XG inverts the pair: the LSB carries the variation and lands in bank, while the MSB chooses between melodic (below 0x7E), the SFX voice bank (0x40), the SFX kits (0x7E) and the drum kits (0x7F). It needs its own field because it decides melodic-versus-drum for the part and so has to survive until the next program change, which is when that decision is taken.

-1 means no bank select has been sent, which is not the same as zero. A program change on a part whose bank was never written must not decide drum routing at all – it leaves the part on its default, so channel 10 stays drums the way XG starts it. Reading an unwritten bank as 0 would silently make every default drum part melodic, which is exactly what a file that sends nothing but program changes would suffer.

◆ poly_pressure

std::array<std::uint8_t, 128> ts::Part::poly_pressure {}

Polyphonic aftertouch, per key.

One byte a note rather than one a part, which is the whole of what makes it polyphonic: the engine's poly_aftertouch_apply takes the part's matrix depths and the pressure belonging to that key, so two notes held on one part can be modulated by different amounts.

Not cleared by note-on, which is the opposite of the obvious guess and was measured rather than assumed. Press a key to full pressure, release it, strike it again and say nothing about pressure: the module sounds the new note still bent, an octave and a half up on a part whose 40 2x 30 depth is 0x58. The pressure belongs to the key, not to the note that happened to be sounding on it, and only another poly message or a reset moves it.

◆ vibrato_rate

int ts::Part::vibrato_rate = 0x40

The GS part modify offsets, 0x40 centred.

Three writers share each one – the sound controllers (CC#71-78), the NRPNs (01 08-01 66), and the part SysEx (40 1x 30-37) all land on the same per-part byte in the engine (part+0x3e4-0x3eb).

Seven of the eight reach the synthesis chains through modifiers().

◆ vibrato_depth

int ts::Part::vibrato_depth = 0x40

◆ vibrato_delay

int ts::Part::vibrato_delay = 0x40

◆ tvf_cutoff

int ts::Part::tvf_cutoff = 0x40

◆ env_attack

int ts::Part::env_attack = 0x40

◆ env_decay

int ts::Part::env_decay = 0x40

◆ env_release

int ts::Part::env_release = 0x40

◆ tvf_resonance

int ts::Part::tvf_resonance = 0x40

The eighth: CC#71 / NRPN 01 21 / 40 1x 33, and the one that goes nowhere.

Write-only to match the engine, which stores it at part+0x3e7 and never reads it. See PartModifiers for why that is a finding rather than an omission. Tracked here anyway so the value survives a round trip, as it does on the module.

◆ scale_tuning

std::array<int, 12> ts::Part::scale_tuning
Initial value:
{
0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40}

GS scale tuning (40 1x 40-4B), one entry per pitch class, 0x40 centred, a cent a step.

◆ rhythm

int ts::Part::rhythm = -1

GS use-for-rhythm (40 1x 15): -1 follows the channel default, 0 forces melodic, 1 or 2 route the part to the drum path on that drum map.

◆ key_low

int ts::Part::key_low = 0

GS keyboard range (40 1x 1D/1E); note-ons outside it are ignored on melodic parts.

◆ key_high

int ts::Part::key_high = 0x7F

◆ eq_enabled

bool ts::Part::eq_enabled = false

Whether this part runs through the four-band EQ (40 4x 20), which is part+0x450.

Off by default, which contradicts the SC-8820 manual. The manual gives this parameter a default of 01 ON; in this engine the part reset writes the byte to zero and nothing anywhere sets it to one except the SysEx handler, so a module that is never told to switch the EQ on never switches it on. That is a difference without a sound until a file also sends a non-flat 40 02, since a flat EQ is exactly transparent either way — but on such a file it decides whether the EQ is heard at all, so it is worth an oracle check before it is trusted.

◆ efx_enabled

bool ts::Part::efx_enabled = false

Whether this part feeds the insertion EFX block (40 4x 22), which is part+0x452.

An EFX part's dry signal detours to the EFX input pair and both of its sends are forced to the null bus — the block's own 40 03 1719 send levels replace them, which is the mechanism behind the manual's note that system-effect levels become common to all EFX parts.

◆ velocity_depth

int ts::Part::velocity_depth = 0x40

GS velocity sense depth and offset (40 1x 1A/1B), applied by effective_velocity.

◆ velocity_offset

int ts::Part::velocity_offset = 0x40

◆ pitch_offset_fine

int ts::Part::pitch_offset_fine = 0x08

GS pitch offset fine (40 1x 17), stored raw: the unit is Hz on the module, which nothing downstream consumes yet.

◆ rpn_msb

int ts::Part::rpn_msb = 0x7F

◆ rpn_lsb

int ts::Part::rpn_lsb = 0x7F

◆ nrpn_msb

int ts::Part::nrpn_msb = 0x7F

◆ nrpn_lsb

int ts::Part::nrpn_lsb = 0x7F

The selected NRPN's LSB, which for the drum parameters is the key number.

◆ data_entry_is_nrpn

bool ts::Part::data_entry_is_nrpn = false

Whether a CC#6 data entry commits to the selected NRPN rather than the selected RPN.

The two share data entry, so the last selection made decides. Tracking this is what keeps a file's drum NRPNs out of the bend range.

◆ drum_keys

DrumKeyOverrides ts::Part::drum_keys

Per-drum-key overrides this part has taken from NRPN.

◆ sustained

std::vector<int> ts::Part::sustained

Notes whose release is waiting for the damper to lift.

Insertion-ordered rather than a set: notes are released oldest first when the pedal comes up, which is the order the offline renderer closes them in too.

◆ portamento_time

int ts::Part::portamento_time = 0

CC#5 portamento time; indexes the glide-step table.

◆ portamento_on

bool ts::Part::portamento_on = false

Whether CC#65 portamento is on.

◆ mono

bool ts::Part::mono = false

Whether CC#126 mono mode is on, which flushes the part's voices at each note-on.

◆ soft

bool ts::Part::soft = false

Whether the CC#67 soft pedal is down.

Binary – the engine reads only bit 6 (cc67_soft_pedal). Latched but not yet consumed by the voice path.

◆ portamento_control_key

int ts::Part::portamento_control_key = -1

CC#84 portamento control: the key the next note glides from, or -1 when unset.

One-shot — the engine consumes it at the next note-on and resets the byte, so it glides exactly one note and does not latch a mode.

◆ last_key

int ts::Part::last_key = -1

The key the part last sounded, which portamento glides from, or -1.

◆ sostenuto_down

bool ts::Part::sostenuto_down = false

Whether the sostenuto pedal is down. CC#66 is binary — bit 6 only, as the engine reads it.

◆ sostenuto_captured

std::vector<int> ts::Part::sostenuto_captured

Notes the sostenuto pedal captured — the ones sounding when it went down.

◆ sostenuto_released

std::vector<int> ts::Part::sostenuto_released

Captured notes whose note-off arrived while the pedal held them.


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