Skip to main content

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_level dynamic to quickly decrease to zero using an io_envelope (explained below). This causes the noise to fade out quickly.
  • Frequency sweeps: To make a falling sound effect, the oscillator_hz dynamic could decrease over time using an envelope.
  • Pitched notes: To make music by playing notes at different pitches, we can use pitch_factor to adjust the oscillator_hz dynamic as each note is triggered. We could also add "vibrato" by making the pitch rise and fall slightly, by applying a looping envelope to oscillator_hz as well.
  • Pulse modulation: A boring pulse oscillator becomes a fun retro synth if you use an envelope to repeatedly expand and contract its pulse_width parameter.
  • Timbre variation: An instrument might use mod_factor to 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_level and right_level parameters. By reversing the low and high range for right_level, it will fade out as left_level fades 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 low and high value that specify a range for the parameter. For most parameters, the pair value 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 high value is used. The low value is ignored.
  • Whenever a note is played, the "TRIGGER INSTRUMENT" command includes a pitch_factor and mod_factor. For a piano, pitch_factor might specify which piano key was pressed, whereas mod_factor specifies how loud it should be (how hard the piano key was pressed). In general, pitch_factor and mod_factor can 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_factor and/or mod_factor. Therefore, the final parameter value can exceed the lowhigh range.

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 (lowhigh). 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, then t might exceed its limit of 255. If samples_per_t is too big, then our graph may be inaccurate because the granularity of t is too big. The Hybrix Designer lets you specify any duration and points, then it calculates samples_per_t such that:

  • samples_per_t cannot exceed 32767
  • the longest segment must be representable as tsamples_per_t where t cannot exceed 255
  • t_sumsamples_per_t should be very close to the desired envelope duration
  • ensuring accuracy of duration_in_samples is more important than perfecting any individual segment

How would you solve that problem?