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

Renders one note by assembling the whole voice chain: sampler, pitch, filter, amplitude and pan. More...

#include <tabulasonora/note_renderer.hpp>

Classes

struct  Controllers
 Optional per-note controller curves. More...

Public Member Functions

 NoteRenderer (const RomImage &rom)
 Creates a note renderer over an open ROM image, which must outlive it.
 NoteRenderer (NoteRenderer &&) noexcept
NoteRendereroperator= (NoteRenderer &&) noexcept
 NoteRenderer (const NoteRenderer &)=delete
NoteRendereroperator= (const NoteRenderer &)=delete
 ~NoteRenderer ()
const RomImagerom () const noexcept
 The ROM this renderer was opened on.
const TableSettables () const noexcept
 The static tables this renderer loaded.
EngineNoisenoise () noexcept
 The engine's shared pseudo-random source.
const PatchDirectorydirectory () const noexcept
 The patch directory.
const DrumKitTabledrums () const noexcept
 The drum kits.
const Interpolatorinterpolator () const noexcept
 The 4-tap resampler.
Samplersampler () noexcept
 The wave decoder and player.
const EnvelopeMachineenvelopes () const noexcept
 The shared segment-rate machine, which also decodes the envelope hold clock.
const TvaChaintva () const noexcept
 The amplitude chain.
const TvfChaintvf () const noexcept
 The filter chain.
const PitchChainpitch () const noexcept
 The pitch chain.
const LfoEnginelfo () const noexcept
 The two LFO engines.
const PanLawpan () const noexcept
 The pan law.
RenderedNote render_note (int program, int note, int velocity, double hold_seconds, double tail_seconds, ToneMap map=ToneMap::sc8820, int bank=0, int part_pan=0x40, const Controllers &controllers={})
 Renders one melodic note.
double drum_ring_scale (int note, int kit, int drum_pitch) const
 How much longer a hit needs than the nominal ring, before it is rendered.
RenderedNote render_drum_note (int note, int velocity, double ring_seconds, double tail_seconds=0.4, int kit=0, std::span< const double > volume={}, int drum_pitch=0, std::optional< int > drum_pan=std::nullopt)
 Renders one drum hit.

Static Public Member Functions

static double drum_ring_scale (const DrumKey &key) noexcept
 How much longer a hit needs than the nominal ring, given its coarse pitch.
static int envelope_rate_key (const DrumKey &key, int drum_pitch) noexcept
 The key a drum hit's envelope rates are key-followed by.
static std::vector< double > expand (std::span< const double > ticks, std::size_t sample_count)
 Holds control-rate values across their block, as the engine does.

Static Public Attributes

static constexpr int control_block = 320
 Samples per control tick.
static constexpr int sample_rate = 32000
 Internal sample rate.
static constexpr double max_drum_ring_scale = 16.0
 Largest drum ring scale, so a caller can bound its buffers.

Detailed Description

Renders one note by assembling the whole voice chain: sampler, pitch, filter, amplitude and pan.

Partials sum. Each is an independent voice dispatched into the same accumulation buffer, and there is no divide-by-count anywhere in the mix path — averaging would silently halve every two-partial patch relative to a one-partial one.

A note's timbre is latched at note-on, but volume and pitch keep tracking their controllers, because the engine re-reads those each block. That is why they arrive here as optional per-sample curves rather than scalars.

Constructor & Destructor Documentation

◆ NoteRenderer() [1/3]

ts::NoteRenderer::NoteRenderer ( const RomImage & rom)
explicit

Creates a note renderer over an open ROM image, which must outlive it.

Tables are read once here, but wave data is read on demand, so releasing the image early fails at the first note rather than at construction.

◆ NoteRenderer() [2/3]

ts::NoteRenderer::NoteRenderer ( NoteRenderer && )
noexcept

◆ NoteRenderer() [3/3]

ts::NoteRenderer::NoteRenderer ( const NoteRenderer & )
delete

◆ ~NoteRenderer()

ts::NoteRenderer::~NoteRenderer ( )

Member Function Documentation

◆ operator=() [1/2]

NoteRenderer & ts::NoteRenderer::operator= ( NoteRenderer && )
noexcept

◆ operator=() [2/2]

NoteRenderer & ts::NoteRenderer::operator= ( const NoteRenderer & )
delete

◆ rom()

const RomImage & ts::NoteRenderer::rom ( ) const
nodiscardnoexcept

The ROM this renderer was opened on.

Exposed because the GS effect parameters are edits to a macro row, and recomputing a network from an edited row means reading the same tables the macro came from. The reference outlives the renderer's caller by construction: a renderer cannot be built without one.

◆ tables()

const TableSet & ts::NoteRenderer::tables ( ) const
nodiscardnoexcept

The static tables this renderer loaded.

◆ noise()

EngineNoise & ts::NoteRenderer::noise ( )
nodiscardnoexcept

The engine's shared pseudo-random source.

One generator serves the whole engine — pitch start jitter, random pan, and the random LFO waveforms all draw from it — so it lives here rather than inside any one chain.

◆ directory()

const PatchDirectory & ts::NoteRenderer::directory ( ) const
nodiscardnoexcept

The patch directory.

◆ drums()

const DrumKitTable & ts::NoteRenderer::drums ( ) const
nodiscardnoexcept

The drum kits.

◆ interpolator()

const Interpolator & ts::NoteRenderer::interpolator ( ) const
nodiscardnoexcept

The 4-tap resampler.

The chains below are exposed so a host can assemble the voice itself — which is what the real-time block loop does, sharing one loaded set of tables and one wave cache with this renderer rather than duplicating either.

◆ sampler()

Sampler & ts::NoteRenderer::sampler ( )
nodiscardnoexcept

The wave decoder and player.

◆ envelopes()

const EnvelopeMachine & ts::NoteRenderer::envelopes ( ) const
nodiscardnoexcept

The shared segment-rate machine, which also decodes the envelope hold clock.

◆ tva()

const TvaChain & ts::NoteRenderer::tva ( ) const
nodiscardnoexcept

The amplitude chain.

◆ tvf()

const TvfChain & ts::NoteRenderer::tvf ( ) const
nodiscardnoexcept

The filter chain.

◆ pitch()

const PitchChain & ts::NoteRenderer::pitch ( ) const
nodiscardnoexcept

The pitch chain.

◆ lfo()

const LfoEngine & ts::NoteRenderer::lfo ( ) const
nodiscardnoexcept

The two LFO engines.

◆ pan()

const PanLaw & ts::NoteRenderer::pan ( ) const
nodiscardnoexcept

The pan law.

◆ render_note()

RenderedNote ts::NoteRenderer::render_note ( int program,
int note,
int velocity,
double hold_seconds,
double tail_seconds,
ToneMap map = ToneMap::sc8820,
int bank = 0,
int part_pan = 0x40,
const Controllers & controllers = {} )
nodiscard

Renders one melodic note.

◆ drum_ring_scale() [1/2]

double ts::NoteRenderer::drum_ring_scale ( const DrumKey & key)
staticnodiscardnoexcept

How much longer a hit needs than the nominal ring, given its coarse pitch.

The ring exists because a drum ignores note-off, so the renderer has to decide for itself how long to run. A fixed window is wrong once a key is pitched down: the sample plays proportionally slower, so the hit outlasts it and is cut off mid-decay. Measured on the real DLL, the splash at NRPN 18h = 24 takes 4.68 s to fall 40 dB against 1.15 s untouched — four times the nominal 1.8 s ring. The envelope, not this window, is what should end the note.

◆ drum_ring_scale() [2/2]

double ts::NoteRenderer::drum_ring_scale ( int note,
int kit,
int drum_pitch ) const
nodiscard

How much longer a hit needs than the nominal ring, before it is rendered.

Lets a caller size its own buffers to the same window the render will use.

◆ render_drum_note()

RenderedNote ts::NoteRenderer::render_drum_note ( int note,
int velocity,
double ring_seconds,
double tail_seconds = 0.4,
int kit = 0,
std::span< const double > volume = {},
int drum_pitch = 0,
std::optional< int > drum_pan = std::nullopt )
nodiscard

Renders one drum hit.

The note does not transpose the sample: the tone resolves at key 60 and the kit's coarse-pitch plane supplies an offset at half strength. The kit level is not part of the voice gain — it enters downstream, squared.

◆ envelope_rate_key()

int ts::NoteRenderer::envelope_rate_key ( const DrumKey & key,
int drum_pitch )
staticnodiscardnoexcept

The key a drum hit's envelope rates are key-followed by.

Not 60, and not the MIDI note. The engine keeps this at voice+0x161, filled by voice_trigger_partials as the note plus a transpose, and on a drum part the sum works out to the kit's own coarse-pitch plane. Read back off the DLL for the 61 sounding keys of the SC-55 standard kit, the plane equals the engine's index for all 61.

The NRPN moves it a whole semitone per step, where the same step moves the pitch plane by two — so the offset is added here rather than reading the modified plane back.

◆ expand()

std::vector< double > ts::NoteRenderer::expand ( std::span< const double > ticks,
std::size_t sample_count )
staticnodiscard

Holds control-rate values across their block, as the engine does.

Member Data Documentation

◆ control_block

int ts::NoteRenderer::control_block = 320
staticconstexpr

Samples per control tick.

◆ sample_rate

int ts::NoteRenderer::sample_rate = 32000
staticconstexpr

Internal sample rate.

◆ max_drum_ring_scale

double ts::NoteRenderer::max_drum_ring_scale = 16.0
staticconstexpr

Largest drum ring scale, so a caller can bound its buffers.


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