The GS controller assignment matrix (40 2x): six sources, each with eleven depths.
This is the module's modulation routing — how far the mod wheel bends the pitch, how much aftertouch opens the filter, how deep CC1 drives LFO2, and so on. The engine keeps one eleven-byte block per source starting at part+0x3fc and hands the block to modmatrix_apply_linear or modmatrix_apply_bipolar with the controller's current amount; the result is eleven signed modulation values the voice adds to its own.
The depths are 0x40-centred, so a source assigned nothing anywhere leaves the voice alone.
Stores one depth the way sysex_part_control_matrix does, which is not a plain assignment.
Ten of the eleven destinations take the byte as it arrives. Pitch does not: it is clamped to 0x28-0x58, which is ±24 semitones, and that range is the reason the pitch law's 0xbe8 clamp is the top of the scale rather than a rail a real stream can reach — 24 × 127 is exactly 3048. Storing the byte raw instead lets a depth of 0x7f reach that clamp at a third of the wheel's travel, so the pitch ramps steeply and then stops dead half way up. That is what it did here until the module was asked: against 40 21 00 at 0x7f the module ramps evenly to 24 semitones across the whole wheel, and this engine had already railed by 64.
Bend's pitch depth never reaches this function — see bend_pitch_lives_in_bend_range. The engine clamps that one to 0x40-0x58 instead, one-sided, which is the 0-24 range Part::bend_range already holds it to.
| Modulation ts::ControlMatrix::applied_linear |
( |
Source | source, |
|
|
int | amount ) const |
|
inlinenodiscardnoexcept |
Scales this source's depths by a controller amount — modmatrix_apply_linear.
Used by every source except bend, which has its own law (modmatrix_apply_bipolar, a per-destination 16-bit scale rather than a shift) and is not this function.
This reads more simply than the engine's version, and the reason is worth knowing rather than trusting. The engine permutes twice: sysex_part_control_matrix writes SysEx destination 3 to block byte 4 and destination 10 to block byte 11, skipping byte 3 altogether; then modmatrix_apply_linear reads block byte 5 into output 6 and byte 7 into output 4, running each LFO group backwards. Composed, the two cancel — storing by SysEx index and naming the outputs makes both disappear. The check that this is a real cancellation and not a coincidence is that the laws then line up with the published ranges: every destination that comes out bipolar is one the manual documents as ±something, and every quartered one is documented as a 0-upward amount.
Three scalings, not one. Pitch takes the product whole; the two continuous destinations and the LFO rates take it halved; the six LFO depths take it quartered and are unipolar — they are amounts rather than offsets, so they are not measured from 0x40. The halving is written as a magnitude shift with the sign reapplied, because an arithmetic shift of a negative rounds toward minus infinity and the engine's does not.
Scales this source's depths by a signed amount — modmatrix_apply_bipolar.
Bend's law, and only bend's. It is not a variant of the linear apply: where that one shifts, this one multiplies by a per-destination 16-bit constant and takes the high word, and its amount is a signed 14-bit deflection rather than a 7-bit controller.
The three constants turn out to be the same three-way split the linear apply makes. Against the << 2 the magnitude already carries, 0xfe16 is ×1, 0x7f00 is ×½ and 0x3f81 is ×¼ — whole for pitch, halved for the continuous destinations and the LFO rates, quartered for the six LFO depths. That the two functions agree on which destination gets which treatment, by completely different arithmetic, is the check that neither has been misread.
The sign is the product of two: the depth's side of centre and the amount's. A downward bend through a negative depth pushes pitch up, which is the point of an inverted assignment. pitch_depth supplies the cell this matrix does not store — bend's, which lives in Part::bend_range. Passing it in keeps that one store rather than mirroring it here.
|
|
inlinestaticnodiscardconstexprnoexcept |
The three constants for one destination.
Two things are worth reading off this table rather than trusting. The clamps are exactly the largest sum each destination can reach: 0xbe8 is 127 x 24 for a whole product, 4000 is 127 x 63 / 2 for a halved one, and 0xfc0 is 127 x 127 / 4 for a quartered one — so the clamp is not a safety rail that a real stream might hit, it is the top of the scale. And each clamp << shift lands just inside 16 bits (64512 is the largest, for the LFO amplitude depths), which is what keeps the engine's unsigned-short intermediate from wrapping. Both facts fall out of the constants; neither was put there by hand.
The full-scale results are the published ranges, exactly: +-24000 milli-semitones of pitch, +-24576 cutoff units (9600 cents at 2.56 a cent), +-32512 of 0x7f00 amplitude, +-6553 of LFO increment (10 Hz, since 65536 increments a tick at 100 Hz is 100 Hz), and for the LFO depths 32512, 6144 and 6000 — 100 %, 2400 cents and 600 cents. Eleven constants reproducing seven documented figures is the check that the table has been read off correctly.
| bool ts::ControlMatrix::bend_pitch_lives_in_bend_range = true |
|
staticconstexpr |
Bend's pitch depth is not stored here, and this says so out loud.
40 2x 10 (BEND PITCH CONTROL) and RPN 00/00 (pitch bend sensitivity) are not two parameters that happen to agree — they are one byte, part+0x408, written by both handlers with the same 0–24 semitone clamp. The engine's bend range is this matrix cell. Keeping a second copy here would be a second source of truth for one value, so Part::bend_range owns it and both messages write there.