|
Tabula Sonora 0.1.0
A native C++20 implementation of the Roland Sound Canvas VA synth voice
|
The filter side of a voice: the cutoff chain that turns static bytes into the two coefficients the state-variable filter needs, and the envelope that moves the cutoff over time. More...
#include <tabulasonora/tvf_chain.hpp>
Classes | |
| struct | Offsets |
| The envelope's peak and its five stage offsets, in cutoff units relative to that peak. More... | |
| struct | Envelope |
| A cutoff envelope and the base cutoff its offsets are added to. More... | |
| struct | Coefficients |
| The pair of coefficients the filter runs on for one control block. More... | |
Public Member Functions | |
| TvfChain (const TableSet &tables, const EnvelopeMachine &envelope) | |
| Creates the chain over a loaded table set and the shared machine, both of which must outlive it. | |
| FilterTap | tap (int filter_type) const noexcept |
| Which filter response a partial takes, or FilterTap::bypass. | |
| int | cutoff_units (double cutoff15, int resonance_byte) const noexcept |
| Warps a 15-bit cutoff sum and clamps it to the resonance-dependent ceiling — stage C. | |
| double | frequency_coefficient (int units) const noexcept |
| The filter's frequency coefficient f. | |
| double | damping_coefficient (int units, int resonance_byte, int filter_type) const noexcept |
| The filter's damping coefficient q — stage D. | |
| Coefficients | coefficients (int units, int resonance_byte, int filter_type) const noexcept |
| Both coefficients at once, with f clamped to the stability ceiling q selects. | |
| std::span< const std::int32_t > | ramp_exp () const noexcept |
| The shared g_ramp_exp_tbl. | |
| double | cutoff_hz (double cutoff15, int resonance_byte, int sample_rate=32000) const noexcept |
| The cutoff in Hz. Diagnostic only — the filter never needs it. | |
| int | effective_velocity (const PartialParameters &partial, int velocity) const noexcept |
| The velocity the filter envelope's depth actually responds to. | |
| Offsets | envelope_offsets (const PartialParameters &partial, int key, int velocity) const |
| The envelope's peak and its five stage offsets. | |
| Envelope | create_envelope (const PartialParameters &partial, int velocity, int key, int sample_rate=32000, const PartModifiers &modifiers={}) const |
| Builds the cutoff envelope for one note, ready to be evaluated at any sample position. | |
| std::vector< double > | envelope (const PartialParameters &partial, int velocity, int key, double hold_seconds, double tail_seconds, int sample_rate=32000) const |
| The 15-bit cutoff trajectory over a note, clamped to 15 bits. | |
| void | apply (std::span< float > signal, std::span< const double > cutoff15, int filter_type, int resonance_byte, int block_samples=control_block_samples) const noexcept |
| Filters a signal in place with a cutoff that moves per control block. | |
Static Public Member Functions | |
| static int | resonance_byte (const PartialParameters &partial, int part_resonance=0x40, int part_resonance_default=0x40) noexcept |
| The resonance byte — stage A. Floored at 4. | |
| static bool | responds_to_env_modifiers (const PartialParameters &partial) noexcept |
| Whether this partial's filter envelope follows the part's envelope modify offsets. | |
Static Public Attributes | |
| static constexpr int | control_block_samples = 320 |
| Samples between coefficient refreshes — the 100 Hz control tick. | |
The filter side of a voice: the cutoff chain that turns static bytes into the two coefficients the state-variable filter needs, and the envelope that moves the cutoff over time.
The cutoff runs through four stages. A derives the resonance byte, B sums the base with the envelope, C warps the sum and clamps it to a resonance-dependent ceiling, and D produces the Q trim. The warped result then passes through an exponential table before it reaches the filter, which is the step an earlier attempt missed: feeding linear cutoff units to a Chamberlin filter drives f to about 1.9, where the state matrix diverges.
Cutoff is note-independent. There is no key-follow on it; brightness tracks the keyboard through the multisample instead.
| ts::TvfChain::TvfChain | ( | const TableSet & | tables, |
| const EnvelopeMachine & | envelope ) |
Creates the chain over a loaded table set and the shared machine, both of which must outlive it.
|
nodiscardnoexcept |
Which filter response a partial takes, or FilterTap::bypass.
Only types 0, 1, 2, 4, 5 and 6 are valid; 3 and 7 bypass. The response comes from bits 10–11 of the type's coefficient word, so the mapping is not the obvious one — type 1 is highpass and type 2 is bandpass.
|
staticnodiscardnoexcept |
The resonance byte — stage A. Floored at 4.
|
nodiscardnoexcept |
Warps a 15-bit cutoff sum and clamps it to the resonance-dependent ceiling — stage C.
The ceiling at neutral resonance times four is 245,760 — the "fully open" constant an earlier calibration measured empirically. It is a ceiling, not a saturation of the sum.
|
nodiscardnoexcept |
The filter's frequency coefficient f.
Every ramp target passes through the exponential table before it reaches the filter, so what the filter sees is exponential in the cutoff units. This collapses to f = 2^(C/16384 - 15), and Chamberlin's f = 2*sin(pi*fc/fs) then gives the cutoff in Hz with no fitted constant at all. The table entries reach 2^18, so the interpolation must be widened before the shift.
|
nodiscardnoexcept |
The filter's damping coefficient q — stage D.
This is reciprocal-Q, so the neutral resonance byte 0x40 yields exactly 1.0 and smaller values are more resonant — effectively Q = 64 / resonance_byte.
|
nodiscardnoexcept |
Both coefficients at once, with f clamped to the stability ceiling q selects.
The engine couples the two: voice_ctrl_ramp_d ramps q and, from the same value, clamps the f that voice_ctrl_ramp_c has just written — f = min(f, g_svf_f_ceil[q_raw >> 8]). The ceiling is Chamberlin's own stability bound, sqrt(q^2 + 4) − q, so the clamp keeps the filter out of the region where the loop diverges. Anything that runs the filter must take its coefficients from here rather than from the two accessors separately.
The clamp is inert over every cutoff and resonance the engine can reach: cutoff_units already holds f below the ceiling for all three q branches, though only just — filter type 6 at resonance byte 4 comes within 0.78%. It is implemented because it is a real stage, not because it changes a render.
|
inlinenodiscardnoexcept |
The shared g_ramp_exp_tbl.
The pitch ramp decodes its sampler increment from the same table and the same octave size, so it is exposed here rather than loaded twice.
|
nodiscardnoexcept |
The cutoff in Hz. Diagnostic only — the filter never needs it.
|
nodiscardnoexcept |
The velocity the filter envelope's depth actually responds to.
Velocity does not reach the depth scaler raw: block[0x2e] picks one of sixteen response curves first. Row 0 is the identity, so the majority of the library is unaffected — Trumpet selects it and is exact either way. Brass 1 selects row 1, which reads velocity 100 as 71.
Using raw velocity leaves Brass 1's filter about a third of an octave too open for the whole note, which measures as +3.5 dB at 4–8 kHz and +6.3 dB above it.
|
nodiscard |
The envelope's peak and its five stage offsets.
|
nodiscard |
Builds the cutoff envelope for one note, ready to be evaluated at any sample position.
The running level starts at zero rather than at the release level: all five targets are made relative to the peak, and the peak itself is folded into the base cutoff instead. TVF segments are always linear — unlike the TVA, the shape is not data-driven here.
modifiers supplies the part's cutoff offset, which always applies, and its envelope offsets, which apply only when the partial opts in — see responds_to_env_modifiers.
|
inlinestaticnodiscardnoexcept |
Whether this partial's filter envelope follows the part's envelope modify offsets.
Bit 4 of block byte 0x0E. tvf_compute_env_rates zeroes its bias outright when the bit is clear, so on those partials CC#73/75/72 move the amplitude envelope and leave the filter envelope alone. The amplitude side has no such gate.
|
nodiscard |
The 15-bit cutoff trajectory over a note, clamped to 15 bits.
|
noexcept |
Filters a signal in place with a cutoff that moves per control block.
Coefficients refresh once per control block, matching the engine's 100 Hz rate. The engine additionally slews them over 2–40 ms through its anti-zipper ramps, which is not modelled.
The coefficients come from the cutoff's mean over the block they will serve, not its value at the tick: the envelope can cross several segments inside one 10 ms tick, and a single sample point costs about 1.7% of peak on a piano attack.
|
staticconstexpr |
Samples between coefficient refreshes — the 100 Hz control tick.