-
Notifications
You must be signed in to change notification settings - Fork 67
Audio
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.
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.
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.
The Saw waveform rises from -32767 to 32768 with the value of the waveform offset.
The value of each sample is just: offset - 0x7fff
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
.
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
.
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
.
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.
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 | | | | | | | | | | | | | | | | |
// +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+--->
The attack phase is the time the note volume takes to go from 0 to the channel volume.
The decay phase is the time the note volume takes to go from the channel volume to the "sustain" volume.
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.
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.
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:
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.
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
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 theATTACK
phase. When the attack time has elapsed, the channel will switch into theDECAY
phase. Generally used for starting a note. -
trigger_decay()
- Switch to theDECAY
phase. When the decay time has elapsed, the channel will switch to theSUSTAIN
phase. -
trigger_sustain()
- Switch to theSUSTAIN
phase. The envelope generator will remain in this phase until another phase is manually triggered. -
trigger_release()
- Switch to theRELEASE
phase generator. When the release time has elapsed, the channel will switchOFF
. 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.
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.
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.
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 thewaveform_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.