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. Jamac'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?