Envelopes & dynamics
Some Jamdac parameters are static parameters, which means they do not change after the instrument is loaded. For example, filter_resonance and digitar_mode can only be changed by reloading the instrument. Other Jamdac parameters are dynamic parameters, which means they can be changed interactively, giving interesting character or "expression" to a sound. In our DSP diagrams, dynamic parameters are shown with an asterisk by the name, for example: REVERB_LEVEL*
A dynamic parameter is configured using an io_dynamic object, which we'll call a dynamic. Here are some example usages:
- Amplitude shaping: To make a snare drum sound, we might start with white noise, and then configure its
output_leveldynamic to quickly decrease to zero using anio_envelope(explained below). This causes the noise to fade out quickly. - Frequency sweeps: To make a falling sound effect, the
oscillator_hzdynamic could decrease over time using an envelope. - Pitched notes: To make music by playing notes at different pitches, we can use
pitch_factorto adjust theoscillator_hzdynamic as each note is triggered. We could also add "vibrato" by making the pitch rise and fall slightly, by applying a looping envelope tooscillator_hzas well. - Pulse modulation: A boring pulse oscillator becomes a fun retro synth if you use an envelope to repeatedly expand and contract its
pulse_widthparameter. - Timbre variation: An instrument might use
mod_factorto make small changes to many different dynamics at once. In this way, even if the same note is played repeatedly, the sound will be a bit different each time. - Stereo effects: We can cause a sound to "pan" from left to right by applying an envelope to both
left_levelandright_levelparameters. By reversing thelowandhighrange forright_level, it will fade out asleft_levelfades in.
Defining a dynamic
# A sound parameter that can be controlled by an io_envelope
class io_dynamic # 5 bytes
# 0..2 = instrument envelopes
# 3..4 = global envelopes
#
# +32 = enable envelope, if disabled then "high" is used
# +64 = enable pitch_factor control
# +128 = enable mod_factor control
var control: byte
# The value when l=0 in the envelope graph
var low: pair
# The value when l=255 in the envelope graph
var high: pair
end class
The io_dynamic class packs all the important information into just 5 bytes:
- The dynamic has a
lowandhighvalue that specify a range for the parameter. For most parameters, thepairvalue will be an S6.10 fixed point number. - The dynamic can be controlled by an envelope, in which case the range determines the envelope's low point (
l=0) and high point (l=255). Because the same envelope might control many different parameters at once, each dynamic needs to provide its own range that is appropriate for the parameter. - If the dynamic is not controlled by an envelope, then the
highvalue is used. Thelowvalue is ignored. - Whenever a note is played, the "TRIGGER INSTRUMENT" command includes a
pitch_factorandmod_factor. For a piano,pitch_factormight specify which piano key was pressed, whereasmod_factorspecifies how loud it should be (how hard the piano key was pressed). In general,pitch_factorandmod_factorcan be applied to any dynamic parameter, so it's up to the instrument to decide what they mean. - The parameter's value gets multiplied by
pitch_factorand/ormod_factor. Therefore, the final parameter value can exceed thelow…highrange.
Defining an envelope
class io_envelope # 20 bytes
var samples_per_t: pair
# 1 = sustain; without this flag, sustain_index is ignored
# 2 = looping
# 4 = stairsteps instead of interpolation
var flags: byte
# Index of the graph point to be held until a "RELEASE INSTRUMENT" command
var sustain_index: byte # [Jamdac Plus]
# Eight (l, t) pairs
# l: 0..255 --> low..high of io_dynamic level
# t: 0..255 --> 0..255*samples_per_t time step size
# If a point has t=0, it creates an open interval with a discontinuity.
# If two consecutive points have t=0, then the second point and remainder
# are discarded. If the eighth point has t>0, it interpolates to l=graph[0].
inset graph: byte[size 16]
end class
Jamdac's envelope component causes a value to change over time according to a specified graph made from line segments. The graph's value at a given time is called the envelope level. The time is measured by counting individual signal samples. Jamdac's envelopes are specified using up to eight data points in io_envelope::graph. Each point determines a level l and a time interval t since the previous point, written as (l, t). The io_envelope::graph elements are bytes, so l ranges from 0…255 (low…high). t is also a byte, however 255 samples would be an extremely short time interval. Instead, t is multiplied by samples_per_t to determine the actual number of samples for the interval. samples_per_t can be any number from 0 to 32767. Representing t as multiples of samples_per_t enables the io_envelope object to fit in just 20 bytes, considerably reducing the MMIO transfer cost.
The envelope duration is the total amount of time for the graph. For a repeating envelope (2 in io_envelope::flags), the envelope duration is the length of one loop. For a given envelope e, its duration can be calculated by adding up the individual t values:
t_sum <-- e.graph[1] + e.graph[3] + e.graph[5] + e.graph[7]
| + e.graph[9] + e.graph[11] + e.graph[13] + e.graph[15]
duration_in_samples <-- t_sum * e.samples_per_t
Thought question: What should we choose for
samples_per_t? If it is too small, thentmight exceed its limit of 255. Ifsamples_per_tis too big, then our graph may be inaccurate because the granularity oftis too big. The Hybrix Designer lets you specify any duration and points, then it calculatessamples_per_tsuch that:
samples_per_tcannot exceed 32767- the longest segment must be representable as
t∗samples_per_twheretcannot exceed 255t_sum∗samples_per_tshould be very close to the desired envelope duration- ensuring accuracy of
duration_in_samplesis more important than perfecting any individual segmentHow would you solve that problem?