Skip to content
Philip Howard edited this page Sep 21, 2020 · 6 revisions

Audio

How 32blit's Audio Works

32blit has 8 simultaneous audio channels capable of any permutation of waveforms from the set: NOISE, SQUARE, SAW, TRIANGLE and SINE.

Additionally a WAVE waveform type allows arbitrary waveform data - such as samples from an audio file - to be played.

Each channel has its own waveform generator with configurable frequency and volume feeding in to an envelope generator and a basic low-pass filter.

Waveform Generator

The waveform generator operates at 22050Hz.

Any permutation of the six available waveform types is supported. The output value is a sum of all the waveforms chosen, divided by the number of active waveforms.

Waveforms are generated using a waveform offset value from 0 to 65535. This offset is incremented upon every audio tick, depending upon the channel frequency: ((f * 256) << 8) / 22050.

For example a frequency of 440 would increment the waveform offset value by 1307 every tick or 28819350 every second, thus overflowing the waveform offset back to 0 at a rate of 440 times a second. Understanding how the waveform offset is incremented is important to understanding how waveforms are generated.

Noise

The Noise waveform does not use the offset directly. Instead, when the the waveform offset overflows to 65536 a new Noise sample is generated as a random integer between -2047 and 2048 using rand(). This sample is then used by the waveform generator.

Saw

The Saw waveform rises from -32767 to 32768 with the value of the waveform offset.

The value of each sample is just: offset - 0x7fff

Triangle

The Triangle waveform rises from -32767 to 32765 while the waveform offset is < 32767:

sample = offset * 2 - 32767

When the waveform offset is >= 32767 the waveform falls from 32767 down to -32769.

Square

An additional pulse_width duty cycle value is used by the Square wave.

When the waveform offset is < pulse_width the Square wave will output 32767.

When the waveform offset is >= pulse_width the Square wave will output -32767.

Sine

For efficiency the Sine waveform uses a built-in lookup table (wavetable) of 256 values, mapping a complete sinusoidal wave starting at -32768.

The sine wave is offset by pi / 2 producing a peak somewhere around the middle of the waveform.

A lookup into the Sine wavetable is calculated by shifting the waveform offset eight places to the right. 65535 >> 8 == 255.

Arbitrary Waveform (Wave)

The arbitrary waveform uses a 64 sample buffer filled by a user callback which is called when the end of the buffer is reached. This function must fill the channel wave_buffer with samples.

A separate wave position value tracks the current position in the buffer.

Envelope Generator

The envelope generator has Attack, Decay and Release phases timed in milliseconds from 0 to 65535 (uint16_t). The sustain volume is a fraction of the channel volume from 0 to 65535.

Together these form the a profile for the loudness of a note at various points during its lifespan. A short, sharp note with a brief period of loudness might sound like a bell or percussive instrument while a long, drawn-out, building note may sound like a string or pad.

  // Attack (750ms) - Decay (500ms) -------- Sustain ----- Release (250ms)
  // 
  //                +         +                                  +    +
  //                |         |                                  |    |
  //                |         |                                  |    |
  //                |         |                                  |    |
  //                v         v                                  v    v
  // 0ms               1000ms              2000ms              3000ms              4000ms
  //                                                                                  
  // |              XXXX |                   |                   |                   |
  // |             X    X|XX                 |                   |                   |
  // |            X      |  XXX              |                   |                   |
  // |           X       |     XXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|                   |
  // |          X        |                   |                   |X                  |
  // |         X         |                   |                   |X                  |
  // |        X          |                   |                   | X                 |
  // |       X           |                   |                   | X                 |
  // |      X            |                   |                   |  X                |
  // |     X             |                   |                   |  X                |
  // |    X              |                   |                   |   X               |
  // |   X               |                   |                   |   X               |
  // |  X +    +    +    |    +    +    +    |    +    +    +    |    +    +    +    |    +
  // | X  |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |
  // |X   |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |    |
  // +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+--->

Attack

The attack phase is the time the note volume takes to go from 0 to the channel volume.

Decay

The decay phase is the time the note volume takes to go from the channel volume to the "sustain" volume.

Sustain

Sustain is the volume at which a note is sustained. On a synthesizer this would be while the key is held down. In 32blit's audio engine this occurs while the note remains triggered.

Release

The release phase is the time it takes the volume to go from the sustain level, back down to 0 when a note is cleared.

Programming 32blit's Audio

32blit does not differentiate between music and sound effects, the 8 audio channels can be reconfigured on the fly to produce different sounds for different purposes.

Channels are presented as an array and are numbered 0 through to 7. The first channel is channels[0], the second is channels[1] and so on.

Each channel has properties for configuring the waveform and envelope:

Waveform & Frequency

The six waveforms are:

  • Waveform::NOISE
  • Waveform::SQUARE
  • Waveform::SAW
  • Waveform::TRIANGLE
  • Waveform::SINE
  • Waveform::WAVE

Waveform combinations are selected via setting or clearing the bits in the waveforms property.

The simplest example of this is picking a single waveform type:

channels[0].waveforms = Waveform::SQUARE;

But multiple waveforms can be selected, too:

channels[0].waveforms = Waveform::SQUARE | Waveform::SAW;

The frequency property determines the frequency in Hz of the resulting waveform- ie: the number of times it repeats in a given second. For example 440 Hz corresponds to the note A4 and the waveform would play 440 times a second.

The pulse_width property applies only to the square wave and governs its duty cycle; the proportion that is high vs low. This is most useful when mixing the square waveform with other waveform types and using it to "gate" them.

Envelope Attack, Decay, Sustain, Release

There are four properties for controlling the channel envelope:

  • attack_ms - The attack duration in milliseconds
  • decay_ms - The decay duration in milliseconds
  • sustain - The sustain volume as a proportion of the channel volume
  • release_ms - The release duration in milliseconds

Playing A Note - Envelope Triggering

The waveform generators in 32blit's audio engine run continuously unless the ADSR phase is OFF, the envelope is responsible for turning their output volume on/off and anywhere in between.

The envelope generator can be switched to any one of its four phases, or a fifth phase off where it waits to be triggered again:

  • trigger_attack() - Switch to the ATTACK phase. When the attack time has elapsed, the channel will switch into the DECAY phase. Generally used for starting a note.
  • trigger_decay() - Switch to the DECAY phase. When the decay time has elapsed, the channel will switch to the SUSTAIN phase.
  • trigger_sustain() - Switch to the SUSTAIN phase. The envelope generator will remain in this phase until another phase is manually triggered.
  • trigger_release() - Switch to the RELEASE phase generator. When the release time has elapsed, the channel will switch OFF. Generally used for stopping a note.
  • off() - Turn off the channel, causing it to do nothing but increment the waveform offset counter

A typical note would begin playing in the envelope's attack phase and naturally fall through the phases until it reaches SUSTAIN where it will stay until the note is re-triggered or turned OFF.

Thus calling channel[x].trigger_attack() is how a note is played.

Short, sharp notes will typically have a sustain volume of 0 so they are not dependent upon manually switching into the OFF state.

Longer notes will typically have a non-zero sustain volume and keep sounding indefinitely. Thus calling channel[x].release() is how a note is stopped, and will drop the notes volume to 0 over the release period.

Directly calling channel[x].off() will cut a note off immediately and turn off the channel. This is effectively the same as a 0ms release period. Doing this may cause audio pops and is not recommended.

Making Sound Effects

The channel frequency and active waveforms can be updated while a note is playing to create interesting results such as note pitch-bend or "boing" sound effects.

Jumping Boing

A basic jumping "boing" sound effect might be accomplished by triggering the attack phase and very quickly ramping up the note frequency. Consider this example:

if(player_has_jumped) {
    channels[1].trigger_attack();
    jump_sweep = 1.0f;
}
if(jump_sweep > 0) {
    channels[1].frequency = 880 - (880.0f * jump_sweep);
    jump_sweep -= 0.05f;
}

This will trigger the channel at 0Hz and then quickly sweep the frequency up to 880Hz.

It relies upon a simple square waveform and a channel configured with a 0 sustain volume so that it does not need to be manually released:

channels[1].waveforms   = Waveform::SQUARE;
channels[1].frequency   = 0;
channels[1].attack_ms   = 30;
channels[1].decay_ms    = 100;
channels[1].sustain     = 0;
channels[1].release_ms  = 0;

The attack and decay are carefully chosen to correspond with the time it takes the game update() loop to complete the frequency sweep.

Hacking Audio

32blit does not prevent messing with the audio engine internals, some interesting values that can be modified on the fly include:

  • noise - the current noise value, normally only updated when the waveform_offset overflows.
  • waveform_offset - the current position within the waveform(s). Ranges from 0 to 32767.
  • wave_buf_pos - the current position within the arbitrary waveform buffer. Ranges from 0 to 63.

While there is a fine line between a popping, distorted mess and clean audio there are no doubt some interesting effects to be found by poking these values.