Tabula Sonora 0.1.0
A native C++20 implementation of the Roland Sound Canvas VA synth voice
Loading...
Searching...
No Matches
control_matrix.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <array>
5#include <cstdlib>
6
7namespace ts {
8
28
43
44 static constexpr int source_count = 6;
45 static constexpr int destination_count = 11;
46
48 static constexpr int neutral = 0x40;
49
52 static constexpr int default_modulation_lfo1_pitch = 0x0A;
53
55 std::array<std::array<int, destination_count>, source_count> depth{};
56
57 ControlMatrix() noexcept { reset(); }
58
60 void reset() noexcept
61 {
62 for (auto& source : depth) {
63 // The three continuous destinations and the two LFO rates are bipolar and centre at
64 // 0x40; the six LFO depths are amounts and start at zero.
65 source = {neutral, neutral, neutral, neutral, 0, 0, 0, neutral, 0, 0, 0};
66 }
68
69 // Bend's pitch depth is deliberately absent here — see `bend_pitch_lives_in_bend_range`.
70 }
71
72 [[nodiscard]] int& at(Source source, Destination destination) noexcept
73 {
74 return depth[static_cast<std::size_t>(source)][static_cast<std::size_t>(destination)];
75 }
76
77 [[nodiscard]] int at(Source source, Destination destination) const noexcept
78 {
79 return depth[static_cast<std::size_t>(source)][static_cast<std::size_t>(destination)];
80 }
81
95 void store(Source source, Destination destination, int value) noexcept
96 {
97 at(source, destination) =
98 destination == Destination::pitch ? std::clamp(value, 0x28, 0x58) : value;
99 }
100
107 struct Modulation {
108 int pitch = 0;
109 int tvf_cutoff = 0;
110 int amplitude = 0;
111 int lfo1_rate = 0;
112 int lfo1_pitch = 0;
113 int lfo1_tvf = 0;
114 int lfo1_tva = 0;
115 int lfo2_rate = 0;
116 int lfo2_pitch = 0;
117 int lfo2_tvf = 0;
118 int lfo2_tva = 0;
119
126 [[nodiscard]] constexpr int at(Destination destination) const noexcept
127 {
128 switch (destination) {
130 return pitch;
132 return tvf_cutoff;
134 return amplitude;
136 return lfo1_rate;
138 return lfo1_pitch;
140 return lfo1_tvf;
142 return lfo1_tva;
144 return lfo2_rate;
146 return lfo2_pitch;
148 return lfo2_tvf;
150 break;
151 }
152 return lfo2_tva;
153 }
154
160 constexpr Modulation& operator+=(const Modulation& other) noexcept
161 {
162 pitch += other.pitch;
163 tvf_cutoff += other.tvf_cutoff;
164 amplitude += other.amplitude;
165 lfo1_rate += other.lfo1_rate;
166 lfo1_pitch += other.lfo1_pitch;
167 lfo1_tvf += other.lfo1_tvf;
168 lfo1_tva += other.lfo1_tva;
169 lfo2_rate += other.lfo2_rate;
170 lfo2_pitch += other.lfo2_pitch;
171 lfo2_tvf += other.lfo2_tvf;
172 lfo2_tva += other.lfo2_tva;
173 return *this;
174 }
175 };
176
197 [[nodiscard]] Modulation applied_linear(Source source, int amount) const noexcept
198 {
199 const auto& row = depth[static_cast<std::size_t>(source)];
200
201 // A bipolar destination, halved. `(|d| * amount) >> 1` with the sign put back.
202 const auto halved = [amount](int value) {
203 const int offset = value - neutral;
204 const int magnitude = (std::abs(offset) * amount) >> 1;
205 return offset < 0 ? -magnitude : magnitude;
206 };
207
208 // A unipolar depth, quartered. No 0x40 offset: zero means none.
209 const auto quartered = [amount](int value) { return (value * amount) >> 2; };
210
211 return Modulation{
212 .pitch = (row[0] - neutral) * amount,
213 .tvf_cutoff = halved(row[1]),
214 .amplitude = halved(row[2]),
215 .lfo1_rate = halved(row[3]),
216 .lfo1_pitch = quartered(row[4]),
217 .lfo1_tvf = quartered(row[5]),
218 .lfo1_tva = quartered(row[6]),
219 .lfo2_rate = halved(row[7]),
220 .lfo2_pitch = quartered(row[8]),
221 .lfo2_tvf = quartered(row[9]),
222 .lfo2_tva = quartered(row[10]),
223 };
224 }
225
242 [[nodiscard]] Modulation
243 applied_bipolar(Source source, int amount, int pitch_depth = neutral) const noexcept
244 {
245 const auto& row = depth[static_cast<std::size_t>(source)];
246 const int magnitude = (amount < 0 ? -amount : amount) << 2;
247 const bool amount_negative = amount < 0;
248
249 // A destination measured from centre, whose sign combines with the amount's.
250 const auto centred = [magnitude, amount_negative](int value, int constant) {
251 const int offset = value - neutral;
252 if (offset == 0) {
253 return 0;
254 }
255 const int scaled =
256 static_cast<int>((static_cast<unsigned>(std::abs(offset) * magnitude) >> 8)
257 * static_cast<unsigned>(constant)
258 >> 16);
259 return (offset < 0) != amount_negative ? -scaled : scaled;
260 };
261
262 // A unipolar depth: zero means none, so only the amount carries a sign.
263 const auto amountwise = [magnitude, amount_negative](int value, int constant) {
264 if (value == 0) {
265 return 0;
266 }
267 const int scaled = static_cast<int>((static_cast<unsigned>(value * magnitude) >> 8)
268 * static_cast<unsigned>(constant)
269 >> 16);
270 return amount_negative ? -scaled : scaled;
271 };
272
273 constexpr int whole = 0xFE16;
274 constexpr int half = 0x7F00;
275 constexpr int quarter = 0x3F81;
276
277 return Modulation{
278 .pitch = centred(pitch_depth, whole),
279 .tvf_cutoff = centred(row[1], half),
280 .amplitude = centred(row[2], half),
281 .lfo1_rate = centred(row[3], half),
282 .lfo1_pitch = amountwise(row[4], quarter),
283 .lfo1_tvf = amountwise(row[5], quarter),
284 .lfo1_tva = amountwise(row[6], quarter),
285 .lfo2_rate = centred(row[7], half),
286 .lfo2_pitch = amountwise(row[8], quarter),
287 .lfo2_tvf = amountwise(row[9], quarter),
288 .lfo2_tva = amountwise(row[10], quarter),
289 };
290 }
291
299 struct Law {
301 int clamp;
303 int shift;
306 };
307
323 [[nodiscard]] static constexpr Law law(Destination destination) noexcept
324 {
325 switch (destination) {
327 return Law{0xBE8, 3, 0xFBF8};
329 return Law{4000, 3, 0xC49C};
331 return Law{4000, 4, 0x820D};
334 return Law{4000, 1, 0xD1B8};
337 return Law{0xFC0, 1, 0xBE7A};
340 return Law{0xFC0, 1, 0xC30D};
343 break;
344 }
345 return Law{0xFC0, 4, 0x8105};
346 }
347
352 [[nodiscard]] static constexpr int scaled(Destination destination, int sum) noexcept
353 {
354 if (sum == 0) {
355 return 0;
356 }
357
358 const Law rule = law(destination);
359 const unsigned magnitude =
360 static_cast<unsigned>(std::min(std::abs(sum), rule.clamp) << rule.shift);
361 const int value =
362 static_cast<int>((magnitude * static_cast<unsigned>(rule.multiplier)) >> 16);
363 return sum < 0 ? -value : value;
364 }
365
367 [[nodiscard]] static constexpr Modulation scaled(const Modulation& sums) noexcept
368 {
369 return Modulation{
370 .pitch = scaled(Destination::pitch, sums.pitch),
371 .tvf_cutoff = scaled(Destination::tvf_cutoff, sums.tvf_cutoff),
372 .amplitude = scaled(Destination::amplitude, sums.amplitude),
373 .lfo1_rate = scaled(Destination::lfo1_rate, sums.lfo1_rate),
374 .lfo1_pitch = scaled(Destination::lfo1_pitch, sums.lfo1_pitch),
375 .lfo1_tvf = scaled(Destination::lfo1_tvf, sums.lfo1_tvf),
376 .lfo1_tva = scaled(Destination::lfo1_tva, sums.lfo1_tva),
377 .lfo2_rate = scaled(Destination::lfo2_rate, sums.lfo2_rate),
378 .lfo2_pitch = scaled(Destination::lfo2_pitch, sums.lfo2_pitch),
379 .lfo2_tvf = scaled(Destination::lfo2_tvf, sums.lfo2_tvf),
380 .lfo2_tva = scaled(Destination::lfo2_tva, sums.lfo2_tva),
381 };
382 }
383
391 static constexpr bool bend_pitch_lives_in_bend_range = true;
392};
393
394} // namespace ts
Definition control_decode.hpp:7
What turns a summed destination into its own unit — part_mod_depth_recalc, once per destination.
Definition control_matrix.hpp:299
int multiplier
The 16.16 multiplier whose high word is the result.
Definition control_matrix.hpp:305
int shift
The left shift the magnitude takes first.
Definition control_matrix.hpp:303
int clamp
The magnitude ceiling, applied before the scale.
Definition control_matrix.hpp:301
What one source contributes, once its depths have been scaled by its current amount.
Definition control_matrix.hpp:107
int pitch
added to the voice's pitch
Definition control_matrix.hpp:108
int amplitude
scales the voice's level
Definition control_matrix.hpp:110
constexpr Modulation & operator+=(const Modulation &other) noexcept
Accumulates another source's contribution.
Definition control_matrix.hpp:160
int lfo1_tva
Definition control_matrix.hpp:114
int lfo2_pitch
Definition control_matrix.hpp:116
int lfo2_tva
Definition control_matrix.hpp:118
constexpr int at(Destination destination) const noexcept
One field, chosen by destination.
Definition control_matrix.hpp:126
int lfo1_rate
Definition control_matrix.hpp:111
int lfo2_tvf
Definition control_matrix.hpp:117
int lfo2_rate
Definition control_matrix.hpp:115
int lfo1_pitch
Definition control_matrix.hpp:112
int lfo1_tvf
Definition control_matrix.hpp:113
int tvf_cutoff
added to the cutoff sum
Definition control_matrix.hpp:109
static constexpr bool bend_pitch_lives_in_bend_range
Bend's pitch depth is not stored here, and this says so out loud.
Definition control_matrix.hpp:391
Source
Where a modulation comes from, in the order 40 2x addresses them.
Definition control_matrix.hpp:20
@ poly_pressure
polyphonic aftertouch, 40 2x 30-3A
Definition control_matrix.hpp:24
@ cc2
the part's CC2, 40 2x 50-5A
Definition control_matrix.hpp:26
@ modulation
CC#1, 40 2x 00-0A.
Definition control_matrix.hpp:21
@ cc1
the part's CC1, 40 2x 40-4A
Definition control_matrix.hpp:25
@ bend
pitch bend, 40 2x 10-1A
Definition control_matrix.hpp:22
@ channel_pressure
channel aftertouch, 40 2x 20-2A
Definition control_matrix.hpp:23
void reset() noexcept
Returns every route to power-on.
Definition control_matrix.hpp:60
int & at(Source source, Destination destination) noexcept
Definition control_matrix.hpp:72
ControlMatrix() noexcept
Definition control_matrix.hpp:57
static constexpr int neutral
The value every depth centres on, and the value of an unassigned route.
Definition control_matrix.hpp:48
static constexpr Modulation scaled(const Modulation &sums) noexcept
Every destination's summed sources, each scaled by its own law.
Definition control_matrix.hpp:367
void store(Source source, Destination destination, int value) noexcept
Stores one depth the way sysex_part_control_matrix does, which is not a plain assignment.
Definition control_matrix.hpp:95
static constexpr int destination_count
Definition control_matrix.hpp:45
static constexpr int scaled(Destination destination, int sum) noexcept
Clamps and scales a destination's summed sources into that destination's unit.
Definition control_matrix.hpp:352
Modulation applied_linear(Source source, int amount) const noexcept
Scales this source's depths by a controller amount — modmatrix_apply_linear.
Definition control_matrix.hpp:197
std::array< std::array< int, destination_count >, source_count > depth
[source][destination].
Definition control_matrix.hpp:55
Modulation applied_bipolar(Source source, int amount, int pitch_depth=neutral) const noexcept
Scales this source's depths by a signed amount — modmatrix_apply_bipolar.
Definition control_matrix.hpp:243
int at(Source source, Destination destination) const noexcept
Definition control_matrix.hpp:77
static constexpr int source_count
Definition control_matrix.hpp:44
static constexpr int default_modulation_lfo1_pitch
The mod wheel's LFO1 pitch depth at power-on — the one destination that is not zero or centred,...
Definition control_matrix.hpp:52
static constexpr Law law(Destination destination) noexcept
The three constants for one destination.
Definition control_matrix.hpp:323
Destination
What a modulation reaches. The index is the low nibble of the SysEx address.
Definition control_matrix.hpp:30
@ lfo1_pitch
0…600 cents
Definition control_matrix.hpp:35
@ lfo2_tva
0…100 %
Definition control_matrix.hpp:41
@ lfo2_tvf
0…2400 cents
Definition control_matrix.hpp:40
@ lfo2_rate
-10…+10 Hz
Definition control_matrix.hpp:38
@ lfo1_tvf
0…2400 cents
Definition control_matrix.hpp:36
@ pitch
-24…+24 semitones
Definition control_matrix.hpp:31
@ lfo1_rate
-10…+10 Hz
Definition control_matrix.hpp:34
@ lfo1_tva
0…100 %
Definition control_matrix.hpp:37
@ amplitude
-100…+100 %
Definition control_matrix.hpp:33
@ lfo2_pitch
0…600 cents
Definition control_matrix.hpp:39
@ tvf_cutoff
-9600…+9600 cents
Definition control_matrix.hpp:32