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

The real-time engine: MIDI in, audio out, rendered a block at a time. More...

#include <tabulasonora/tone_generator.hpp>

Public Member Functions

 ToneGenerator (NoteRenderer &notes, const ToneGeneratorOptions &options={})
 Creates an engine over a note renderer's loaded tables, which must outlive it.
 ToneGenerator (ToneGenerator &&) noexcept
ToneGeneratoroperator= (ToneGenerator &&) noexcept
 ToneGenerator (const ToneGenerator &)=delete
ToneGeneratoroperator= (const ToneGenerator &)=delete
 ~ToneGenerator ()
double output_gain () const noexcept
 Linear gain applied to the audio handed to the host.
void set_output_gain (double gain) noexcept
int ports () const noexcept
 How many ports this engine was created with; parts are ports() * 16.
int parts () const noexcept
 How many parts this engine was created with.
std::int64_t position () const noexcept
 How many samples have been rendered since the last reset.
int note_count () const noexcept
 How many notes have sounded since the last reset.
int active_voices () const noexcept
 How many voices are currently sounding, including those fading after being stolen.
bool polyphony_limit_reached () const noexcept
 Whether this render ever ran out of voices.
int stolen_voices () const noexcept
 How many sounding voices were taken to make room.
int voice_slots () const noexcept
 The number of voice slots that exist, after any growth.
const Partpart (int index) const noexcept
 The parts, indexed by port * 16 + channel; parts() of them.
const VoicePoolvoices () const noexcept
 The voice allocator.
int drum_kit () const noexcept
 The drum kit in force, as the last program change on the drum part resolved it.
int drum_kit_for (int port) const noexcept
 The drum kit in force on one port.
std::optional< int > drum_map_row () const noexcept
 Which drum map row a program change on the drum part resolves against.
void set_drum_map_row (std::optional< int > row) noexcept
int effective_drum_map_row () const noexcept
 The drum map row this engine actually resolves against.
bool xg_mode () const noexcept
 Whether the engine is in XG mode, having seen XG System On and no Roland or GM reset since.
ToneMap part_tone_map (int index) const noexcept
 Which tone map a part's program change currently resolves against.
int part_lookup_bank (int index) const noexcept
 The bank a part's melodic lookup is given.
bool part_is_drum (int index) const noexcept
 Whether a part is currently sounding drums.
int part_drum_kit (int index) const noexcept
 The drum kit a part would sound, or -1 if it is not a drum part.
void reset ()
 Silences everything and returns every part to its power-on state.
void send (const MidiEvent &message)
 Applies one MIDI event on port A; its position is ignored, since it applies now.
void send (int port, const MidiEvent &message)
 Applies one MIDI event on a port. Anything past ports() folds onto the ports that exist.
void send_channel (int status, int data1, int data2)
 Applies one channel voice message on port A.
void send_channel (int port, int status, int data1, int data2)
 Applies one channel voice message on a port.
void send_sysex (std::span< const std::uint8_t > bytes)
 Applies one system-exclusive message on port A, including the leading F0.
void send_sysex (int port, std::span< const std::uint8_t > bytes)
 Applies one system-exclusive message on a port.
void send_packet (std::uint32_t packet)
 Applies one USB-MIDI Event Packet: (port << 4) | class in the low byte, then the MIDI message in the three above it, least-significant first.
void render (std::span< float > left, std::span< float > right)
 Renders audio into two equal-length channels.

Static Public Attributes

static constexpr int sample_rate = NoteRenderer::sample_rate
 Internal sample rate.
static constexpr int block_size = smf::block_grid
 Samples per render block — the grid events are applied on.
static constexpr int control_block = NoteRenderer::control_block
 Samples per control tick, at 100 Hz.
static constexpr int port_count = 2
 How many MIDI ports the hardware accepts input on.
static constexpr int part_count = port_count * Sequence::channel_count
 How many parts the hardware has: sixteen per port.
static constexpr int max_port_count = 4
 The widest this engine can be asked to run, past what the module can do.
static constexpr int max_part_count = max_port_count * Sequence::channel_count
 Parts at max_port_count.

Detailed Description

The real-time engine: MIDI in, audio out, rendered a block at a time.

This is the block-based voice loop the hardware runs. Events are applied at the render-block boundary — the grid the engine itself quantises them to — voices are allocated from a pool that holds the hardware's 64 unless ToneGeneratorOptions says otherwise, and each block is summed into a dry pair and three send buses that the effects then process. Nothing about a note has to be known in advance, so a note can be held indefinitely and released whenever.

It shares its DSP with the note renderer rather than reimplementing it: the same envelopes, the same sampler, the same tables. NoteRenderer renders one note in isolation, for analysis and for the gates; this is the only path a song takes, offline or live, which is why an exported render and a live performance of the same file cannot drift apart.

Not thread-safe. Events and rendering must come from the same thread, or be serialised by the caller.

Constructor & Destructor Documentation

◆ ToneGenerator() [1/3]

ts::ToneGenerator::ToneGenerator ( NoteRenderer & notes,
const ToneGeneratorOptions & options = {} )
explicit

Creates an engine over a note renderer's loaded tables, which must outlive it.

◆ ToneGenerator() [2/3]

ts::ToneGenerator::ToneGenerator ( ToneGenerator && )
noexcept

◆ ToneGenerator() [3/3]

ts::ToneGenerator::ToneGenerator ( const ToneGenerator & )
delete

◆ ~ToneGenerator()

ts::ToneGenerator::~ToneGenerator ( )

Member Function Documentation

◆ operator=() [1/2]

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

◆ operator=() [2/2]

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

◆ output_gain()

double ts::ToneGenerator::output_gain ( ) const
nodiscardnoexcept

Linear gain applied to the audio handed to the host.

A trim on the way out, applied where the block is copied to the caller rather than inside the block loop, so no voice, effect or feedback path sees it. reset leaves it alone.

◆ set_output_gain()

void ts::ToneGenerator::set_output_gain ( double gain)
noexcept

◆ ports()

int ts::ToneGenerator::ports ( ) const
nodiscardnoexcept

How many ports this engine was created with; parts are ports() * 16.

◆ parts()

int ts::ToneGenerator::parts ( ) const
nodiscardnoexcept

How many parts this engine was created with.

◆ position()

std::int64_t ts::ToneGenerator::position ( ) const
nodiscardnoexcept

How many samples have been rendered since the last reset.

◆ note_count()

int ts::ToneGenerator::note_count ( ) const
nodiscardnoexcept

How many notes have sounded since the last reset.

A note that resolves to nothing — an unassigned program, or a velocity outside every partial's window — is not counted, since no voice starts.

◆ active_voices()

int ts::ToneGenerator::active_voices ( ) const
nodiscardnoexcept

How many voices are currently sounding, including those fading after being stolen.

◆ polyphony_limit_reached()

bool ts::ToneGenerator::polyphony_limit_reached ( ) const
nodiscardnoexcept

Whether this render ever ran out of voices.

False means the polyphony setting made no difference to the output: nothing was stolen and nothing had to grow, so every larger limit would have produced the same audio. A caller comparing digests across polyphony settings can use this to say whether a match is meaningful or merely a file that never got busy.

◆ stolen_voices()

int ts::ToneGenerator::stolen_voices ( ) const
nodiscardnoexcept

How many sounding voices were taken to make room.

◆ voice_slots()

int ts::ToneGenerator::voice_slots ( ) const
nodiscardnoexcept

The number of voice slots that exist, after any growth.

Not the peak usage: nothing counts how many voices sounded at once, only how many slots were allocated. For a growing pool the two converge, because it only grows when it runs out; for a fixed one this is just the limit.

◆ part()

const Part & ts::ToneGenerator::part ( int index) const
nodiscardnoexcept

The parts, indexed by port * 16 + channel; parts() of them.

The first sixteen are port A and are what a host that never names a port drives, so an engine sent only port-A traffic behaves exactly as a sixteen-part one.

An index past parts() is clamped, not undefined — but it is still a caller's bug, and ChannelMask::channel_count is not the bound to walk: the mask is sixty-four wide whatever the engine is.

◆ voices()

const VoicePool & ts::ToneGenerator::voices ( ) const
nodiscardnoexcept

The voice allocator.

◆ drum_kit()

int ts::ToneGenerator::drum_kit ( ) const
nodiscardnoexcept

The drum kit in force, as the last program change on the drum part resolved it.

Worth reading rather than recomputing: a program the map does not define leaves the kit as it was, so kit_for_program over the part's current program does not always answer what is actually loaded.

◆ drum_kit_for()

int ts::ToneGenerator::drum_kit_for ( int port) const
nodiscardnoexcept

The drum kit in force on one port.

Each port has its own drum part, so each carries its own kit — per (port, map), the module's eight kit buffers; this reports the port's MAP1 kit, the default rhythm part's. drum_kit() is port A's.

◆ drum_map_row()

std::optional< int > ts::ToneGenerator::drum_map_row ( ) const
nodiscardnoexcept

Which drum map row a program change on the drum part resolves against.

The module derives this from the part's internal bank code, and that translation is not reversed — so nothing in a MIDI file reaches it. Until it is, the row is set by the host, which is the only way the second map's kits can be sounded at all.

Deliberately not cleared by reset. The kit is, because a program change selects it and reset undoes what MIDI did; the row is configuration, like the tone map.

◆ set_drum_map_row()

void ts::ToneGenerator::set_drum_map_row ( std::optional< int > row)
noexcept

◆ effective_drum_map_row()

int ts::ToneGenerator::effective_drum_map_row ( ) const
nodiscardnoexcept

The drum map row this engine actually resolves against.

◆ xg_mode()

bool ts::ToneGenerator::xg_mode ( ) const
nodiscardnoexcept

Whether the engine is in XG mode, having seen XG System On and no Roland or GM reset since.

◆ part_tone_map()

ToneMap ts::ToneGenerator::part_tone_map ( int index) const
nodiscardnoexcept

Which tone map a part's program change currently resolves against.

Not ToneGeneratorOptions::map: a bank select LSB names a vintage, and XG mode overrides both and puts every part on the XG map. A display that names programs has to ask per part and per moment, because this changes while the music plays.

◆ part_lookup_bank()

int ts::ToneGenerator::part_lookup_bank ( int index) const
nodiscardnoexcept

The bank a part's melodic lookup is given.

Usually part(i).bank, but not under XG: the bank pair is inverted there, and bank MSB 64 substitutes the SFX voice column rather than any variation the part stored.

◆ part_is_drum()

bool ts::ToneGenerator::part_is_drum ( int index) const
nodiscardnoexcept

Whether a part is currently sounding drums.

The channel number does not answer this. GS routes a part to the drum path with use-for-rhythm SysEx, and XG does it from bank select alone, so under XG any part can be drums and the drum part can be melodic.

◆ part_drum_kit()

int ts::ToneGenerator::part_drum_kit ( int index) const
nodiscardnoexcept

The drum kit a part would sound, or -1 if it is not a drum part.

◆ reset()

void ts::ToneGenerator::reset ( )

Silences everything and returns every part to its power-on state.

◆ send() [1/2]

void ts::ToneGenerator::send ( const MidiEvent & message)

Applies one MIDI event on port A; its position is ignored, since it applies now.

◆ send() [2/2]

void ts::ToneGenerator::send ( int port,
const MidiEvent & message )

Applies one MIDI event on a port. Anything past ports() folds onto the ports that exist.

◆ send_channel() [1/2]

void ts::ToneGenerator::send_channel ( int status,
int data1,
int data2 )

Applies one channel voice message on port A.

The equivalent of the module's TG_ShortMidiIn, which builds a packet with the port field hardwired to zero and so can only ever reach port A.

◆ send_channel() [2/2]

void ts::ToneGenerator::send_channel ( int port,
int status,
int data1,
int data2 )

Applies one channel voice message on a port.

Anything past ports() folds onto those that exist.

The port travels with the message rather than being selected beforehand, which is how the module works: it dispatches on the port field of each packet as that packet is drained, and nothing carries the field over from one message to the next.

◆ send_sysex() [1/2]

void ts::ToneGenerator::send_sysex ( std::span< const std::uint8_t > bytes)

Applies one system-exclusive message on port A, including the leading F0.

◆ send_sysex() [2/2]

void ts::ToneGenerator::send_sysex ( int port,
std::span< const std::uint8_t > bytes )

Applies one system-exclusive message on a port.

GS part addressing is port-relative: a 40 1n block address names a part on whichever port the message arrived on. The module does this by latching the arriving packet's port field into the high nibble of its current-channel global and selecting the part array from it, so the same address means a different part on each port.

◆ send_packet()

void ts::ToneGenerator::send_packet ( std::uint32_t packet)

Applies one USB-MIDI Event Packet: (port << 4) | class in the low byte, then the MIDI message in the three above it, least-significant first.

The equivalent of the module's TG_PMidiIn, which is the only one of its exports that can name a port. The message length is taken from the status byte rather than the class nibble, so a caller that leaves the class at zero still gets the right message.

The port field is masked with 0x1f — the class nibble plus the low bit of the port — so ports 0 and 1 pass through and anything wider folds onto them by its low bit. That is the module's own mask widened by one bit: it ships as 0x0f, which discards the port outright and is why the stock DLL reaches only sixteen of its thirty-two parts.

◆ render()

void ts::ToneGenerator::render ( std::span< float > left,
std::span< float > right )

Renders audio into two equal-length channels.

Any length is accepted. Blocks are still rendered whole and the remainder carried, because a voice counts its control tick in blocks. A caller that wants events to land exactly where the engine would put them should render in multiples of block_size and send between calls.

Throws std::invalid_argument if the two channels differ in length.

Member Data Documentation

◆ sample_rate

int ts::ToneGenerator::sample_rate = NoteRenderer::sample_rate
staticconstexpr

Internal sample rate.

◆ block_size

int ts::ToneGenerator::block_size = smf::block_grid
staticconstexpr

Samples per render block — the grid events are applied on.

◆ control_block

int ts::ToneGenerator::control_block = NoteRenderer::control_block
staticconstexpr

Samples per control tick, at 100 Hz.

◆ port_count

int ts::ToneGenerator::port_count = 2
staticconstexpr

How many MIDI ports the hardware accepts input on.

Two, because the module has thirty-two parts and addresses them as port * 16 + channel. It allocates all thirty-two unconditionally — the part count global is initialised to 0x20 and the second part array sits exactly sixteen strides on from the first — but midi_drain_ready_to_ports masks the port field out of every incoming packet with and r8b,0Fh, so nothing but port A can be reached. Widening that mask to 0x1f admits the second port and no more, which is what this engine implements.

◆ part_count

int ts::ToneGenerator::part_count = port_count * Sequence::channel_count
staticconstexpr

How many parts the hardware has: sixteen per port.

◆ max_port_count

int ts::ToneGenerator::max_port_count = 4
staticconstexpr

The widest this engine can be asked to run, past what the module can do.

ToneGeneratorOptions::ports accepts 1, 2 or 4, giving 16, 32 or 64 parts, and the default is the hardware's 2. Four is an extension, not a fidelity feature: the module allocates thirty-two parts and no more, so nothing above port_count reproduces anything it does. It exists for the same reason unlimited_polyphony does – a host driving more than thirty-two channels of material through one engine, where being able to play the file matters more than matching a module that could not have played it either.

Pair it with the polyphony. Thirty-two parts share the hardware's 64 voices already; sixty- four parts asking for the same 64 will steal continuously and sound nothing like the extra parts were worth having. Something like 256 is the sensible companion, and unlimited_polyphony for offline work.

◆ max_part_count

int ts::ToneGenerator::max_part_count = max_port_count * Sequence::channel_count
staticconstexpr

Parts at max_port_count.


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