General linear-combination control inputs
dpwe opened this issue · comments
Currently, the way that note, envelope, and lfo inputs affect pitch, envelope, and filter cutoff etc. is fairly complex and irregular.
In the spirit of the voltage-summing nodes of analog synths, I want to introduce a fully orthogonal structure, where each voice parameter is calculated as the sum of the same set of control inputs via a matrix of scale coefficients.
For example, instead of filter_freq=1000
setting the cutoff to a fixed value, followed by bp0_target=FILTER_FREQ
and setting up the bp0
envelope to get a sweep, you would do something like:
filter_freq=1000.0,1.0,0,0,0
where the vector of coefficients now indicate the weights for a fixed set of control inputs that are summed together.
The first coefficient is always taken as-is, providing a constant starting point, but the remainder are applied to inputs whose values vary, defined in some fixed order. In the example above, the second value applies to bp0
, but we would also include note value (pitch), note velocity, lfo, etc.
Voice parameters include oscillator frequency, output level, filter frequency, PWM duty, and stereo pan.
I have this approach working in branch linear_combo_ctl
. Each of amp
, freq
, filter_freq
, duty
, and pan
take a list of up to 6 parameters, which are applied to CONST
, NOTE
, VEL
, EG0
, EG1
, and MOD
to calculate the actual values used by the oscillator.
All the calculations are linear sums (param = \sum coefficient * control_input
) except amplitude. To support scaling the envelope generator by the velocity value, we have to multiply together all the non-zero elements. This is irregular and inelegant. It also means the amp coefficients are essentially just binary, i.e. included or not (since any nonzero value other than 1 simply scales the overall amplitude). The fix here would be to have the amplitude also specified in log-units (e.g. dB, although probably something with a range closer to 0..10), but it's impossible to express 0 amplitude in log units (since log(0) = -inf
). However, we could define 0.0 as, e.g., -100 dB (which becomes silence for 16 bit outputs), and 10.0 as 0 dB (nominal full-scale).
To make the behavior sort-of contiguous with how things were before, I also had to introduce some specific defaults:
amp_coefs
defaults to{0, 0, 1, 1, 0, 0}
so that by default the note amplitude is the product of the velocity attribute and the first envelope generator (EG0). EG0 follows the key gate (1 after a nonzero velocity, then 0 after avel=0
event), so it provides the rectangular envelope proportional to velocity. (Note that the VEL control input does not clear to zero at note-off, so any release provided by the EG will continue at the same scale).freq_coefs
defaults to{0, 1, 0, 0, 0, 0}
, i.e. the oscillator frequency follows the note input by default. However, a command such asamy.send(osc=0, freq=1000)
is interpreted asfreq_coefs = {1000, 0, 0, 0, 0, 0}
, i.e., the frequency is made independent of the note attribute. You can, of course, have the frequency be offset from the note input with something likeamy.send(osc=0, freq='16.36,1')
. The constant frequency is represented as log-frequency relative to MIDI 0 (8.18 Hz), so this command makes osc 0 have a frequency one octave above thenote
value.