stevesims / agon-vdp

Official AGON QUARK Firmware: ESP32 VDP
MIT License
0 stars 0 forks source link

Volume envelope (ADSR) support #4

Closed stevesims closed 1 year ago

stevesims commented 1 year ago

Currently the audio system offered by the VDP is simplistic - a note being played is instantly set to its specified volume, and when the duration is complete the volume is set to zero. This doesn't allow for much character...

It is common for sound generation/synthesis systems to support applying an envelope to the volume of a note. The most common envelope type is ADSR, Attack, Decay, Sustain and Release, named after the phases the volume level goes thru on playback.

Note playback using an ADSR envelope requires four values to specify how playback should proceed:

  1. Attack (time in ms)

    During this phase, the playback volume level will be gradually ramped from zero to the target volume specified for the note playback

  2. Decay (time in ms)

    Gradually ramp the volume to the Sustain level

  3. Sustain (volume for sustain phase)

    Note plays at this level until total playback time has reached the duration specified for note playback

  4. Release (time in ms)

    During release, the volume ramps down to zero. Typically this phase is not considered to be part of the note playback duration, and so playback is considered to be complete when this phase starts. A new note can therefore be played during this phase.

NB there are many other types of volume envelope used in sound synthesis, ADSR is just the most common. One such example is the variation on ADSR implemented as part of the original BBC Micro's ENVELOPE command where the sustain phase could also ramp the volume level. We should design our volume envelope system to allow for support different types of envelope to be supported in the future.

stevesims commented 1 year ago

There are a few different approaches for envelope support that we could go with.

  1. Add a note play command variant that allows for ADSR envelope values to be specified as part of the command parameters
  2. Add a command to set ADSR envelope values against a channel (separate to play) so all subsequent notes played on that channel would have the envelope applied
  3. Have a command to define an ADSR envelope that would be stored for later use, and then either allow that stored envelope to be applied to single note playback or a channel as a whole

My inclination is to use 2.

Approach 1 makes for slightly less efficient note playing, and more complex code on the z80 side. Approach 3 may has a slight advantage for reusability, but that is fairly minimal and the extra complexity probably isn't worth it.

stevesims commented 1 year ago

The Sustain value described here is very simplistic, and would mean that all sounds played on a channel with such an envelope applied would play back at the sustain value specified and thus be of uniform volume.

There are potentially other ways to interpret sustain...

We could use the sustain value as a modifier. The most simplistic version of that would be to set the actual sustain phase volume level as noteVolume + sustainValue. Alternatively it could be interpreted as a proportional value, essentially a fraction to apply to the note volume.

Supporting other alternate interpretations of how to deal with sustain will most likely be handled by specifying different envelope types, i.e. different ADSR envelope variants.

Another possibility for different envelope types could be to support different easing functions when transitioning between volume levels (a bit like how transitions/animations can work in CSS)... This idea may be a step too far though. 😁

stevesims commented 1 year ago

Something to consider here is the behaviour when setting an envelope on a channel that is already busy playing a sound. Do we want the envelope to be applied immediately, and thus applied to the currently playing sound, or only for the next sound? Or do we just ignore the request as the channel is busy?

My inclination is to support at least "immediate", but also consider supporting "next".

stevesims commented 1 year ago

volume envelope now supported.

the volume envelope VDU command accepts a type byte, with zero indicating "none", and 1 to specify a simple ADSR envelope.

for an ADSR envelope the command will then look for additional data as parameters to specify the envelope, which are in order: attack, 16-bit value, time in ms to reach specified volume level decay, 16-bit value, time in ms to go to sustain level sustain, 8-bit value, modifier for sustain volume level, centred on 127 release, 16-bit value, time in ms to release

volume level for the sustain phase is calculated as (baseVolume * sustain)/127 which allows for the sustain phase to potentially be louder than the specified volume level

envelope is always applied on playback when one has been set, which has a few important effects.

firstly when triggering an indefinite note playback (either by playing a note with a duration of -1, or setting a volume level on a silenced channel) the attack and decay phases will happen, and the sound will stay at the sustain level until the volume set to zero, which will trigger the "release" phase. if immediate silencing of a channel is wanted then the envelope should be removed.

secondly the "attack+decay" time becomes a minimum duration for a note. using the "play" command with a duration of zero will therefore no longer be silent.

the "release" phase is not included in the note duration, and is interruptible, i.e. during "release" a new note can be played.