|
Tabula Sonora 0.1.0
A native C++20 implementation of the Roland Sound Canvas VA synth voice
|
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 ¬es, const ToneGeneratorOptions &options={}) | |
| Creates an engine over a note renderer's loaded tables, which must outlive it. | |
| ToneGenerator (ToneGenerator &&) noexcept | |
| ToneGenerator & | operator= (ToneGenerator &&) noexcept |
| ToneGenerator (const ToneGenerator &)=delete | |
| ToneGenerator & | operator= (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 Part & | part (int index) const noexcept |
| The parts, indexed by port * 16 + channel; parts() of them. | |
| const VoicePool & | voices () 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. | |
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.
|
explicit |
Creates an engine over a note renderer's loaded tables, which must outlive it.
|
noexcept |
|
delete |
| ts::ToneGenerator::~ToneGenerator | ( | ) |
|
noexcept |
|
delete |
|
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.
|
noexcept |
|
nodiscardnoexcept |
How many ports this engine was created with; parts are ports() * 16.
|
nodiscardnoexcept |
How many parts this engine was created with.
|
nodiscardnoexcept |
How many samples have been rendered since the last reset.
|
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.
|
nodiscardnoexcept |
How many voices are currently sounding, including those fading after being stolen.
|
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.
|
nodiscardnoexcept |
How many sounding voices were taken to make room.
|
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.
|
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.
|
nodiscardnoexcept |
The voice allocator.
|
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.
|
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.
|
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.
|
noexcept |
|
nodiscardnoexcept |
The drum map row this engine actually resolves against.
|
nodiscardnoexcept |
Whether the engine is in XG mode, having seen XG System On and no Roland or GM reset since.
|
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.
|
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.
|
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.
|
nodiscardnoexcept |
The drum kit a part would sound, or -1 if it is not a drum part.
| void ts::ToneGenerator::reset | ( | ) |
Silences everything and returns every part to its power-on state.
| void ts::ToneGenerator::send | ( | const MidiEvent & | message | ) |
Applies one MIDI event on port A; its position is ignored, since it applies now.
| 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.
| 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.
| 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.
| void ts::ToneGenerator::send_sysex | ( | std::span< const std::uint8_t > | bytes | ) |
Applies one system-exclusive message on port A, including the leading F0.
| 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.
| 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.
| 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.
|
staticconstexpr |
Internal sample rate.
|
staticconstexpr |
Samples per render block — the grid events are applied on.
|
staticconstexpr |
Samples per control tick, at 100 Hz.
|
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.
|
staticconstexpr |
How many parts the hardware has: sixteen per port.
|
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.
|
staticconstexpr |
Parts at max_port_count.