Question about ADSR envelopes

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Question about ADSR envelopes
by on (#64915)
How do you guys handle the R(elease) part in engines with dynamic tempo? I mean, I believe it's necessary to know the length of the notes (in frames) to properly apply that last part of the envelope, but I can't think of an efficient way to do it... Any ideas?

by on (#64917)
Release is just a decay state that begins at the note-off command in the sequence. It's not very audible on voice-poor synthesizers like the NES APU because it gets preempted by the next note-on.

by on (#64923)
So you only get to hear the decay of notes that are followed by silence?

by on (#64926)
Some playing styles use an effect called "staccato" in which every note is followed by silence. For example, in S3M and IT, the SC4 effect cuts the playing note 4 frames after the start of this row.

by on (#64959)
Personally I don't think the volume envelope should be tied to the note length. If you think about plucking one string on a guitar, the volume of the sound doesn't decay faster/slower depending on how quickly you pluck the string again.

What you should have is a gate on timer which controls when the envelope passes into the release phase for a held note, or use key-off commands to do the same thing if you want to truncate your note early.

by on (#64963)
Thanks for the insight, Neil. I'm still getting the hand of this music thing!

by on (#64966)
One thing you may want to have though is a rate scaling feature (as seen on some FM chips), where the envelope narrows at higher frequencies, according to a scaling value.

by on (#64971)
To do ADSR, you need to keep track of something called "key-on"

Key On is when the note starts, and Key Off is when the note ends. In most musical situations, you'll be given the option to either use a Key Off, or just a simple Rest to end your notes (which just cuts the notes to silence).

Anyway, to do ADSR, you basically just do this:

Key On!

Volume = whatever
A Phase: Volume += A
Repeat until Volume = Maximum
D Phase: Volume -= D
Repeat until Volume <= S
Volume = S
Wait here until Key Off
R Phase: Volume -= R

End

Key Off is just a command entered by the composer. It's entirely possible for the composer to just ignore adding the Key Offs to the song. In that case, each note will just go through attack and decay, pausing at sustain until the next note, which will interrupt the previous note, and also go through attack and decay.

As for the timing of the envelopes, I think it's best kept independent of the song tempo, like Neil said.

by on (#64974)
In the simple music engine, I don't have ADSR really but I have a system that let the volume decrease a couple of ticks before it ends. On a fast tempo, this will last a couple of frames and won't be very audible, but on slower tempos, this will last quite a few frames, and will be audible.

This is a "cheap" system. On a more advanced one you'd want a system with parametrable note % that is held before the release phase, or a system with key-off commands like MIDI.

by on (#65078)
I rewrote my ADSR routine for this new tracker. I'm quite please with it because it only takes two thirds of a scan-line (worst case).

I'd be interested if anyone could optimise this further while retaining the same functionality.

Link here too, I normally don't use forum-friendly formatting :)

http://dl.dropbox.com/u/5493868/adsr.asm

Code:

;---------------------------------------------------------------
; ADSR Routine
;
; Neil Baldwin, 2010
;
; Taken from my new "Pulsar" project.
; Feel free to use it if it's any use to you
;
;---------------------------------------------------------------
;
; Requires RAM variables
;
;envelopeCounter   DB 0
;envelopeSpeed   DB 0
;envelopeAmp   DB 0
;envelopePhase   DB 0

initPhase      EQU 5   ;set "envelopePhase" to this value to start it
attackPhase      EQU 4
decayPhase      EQU 3
sustainPhase   EQU 2
releasePhase   EQU 1
offPhase      EQU 0   ;envelope stops at this stage

;
;Test ADSR settings
;
;For A, D, S & R, the higher the number, the quicker the speed
;For gateTime, the higher the number, the longer the sustain phase
;
attack   DB $30   ;speed to go from 0 to F amplitude, if speed set to 0, envelope starts at max (0F) amplitude
decay   DB $10   ;speed to go from max (0F) to sustain level
sustain   DB $08   ;level at which envelope is held, see gateTime
release   DB $10   ;speed to go from sustain level to 0
gateTime   DB $40   ;controls the release phase. 0=hold forever, anything else is number of frames
      ;sustain level held until release phase is triggered

;
;Call this routine every audio engine refresh
;
;Of course, if you want to use it on multiple voices, you'll need to either use indexing on the RAM variables
;or turn it into a macro.
;
doADSR:   ldy envelopePhase      ;get phase address (-1)
   lda envelopePhasesHi,y      ;and push onto stack for RTS trick
   pha
   lda envelopePhasesLo,y
   pha
   rts

envelopePhasesLo
   DL adsrOff-1,adsrRelease-1,adsrSustain-1,adsrDecay-1,adsrAttack-1,adsrInit-1
envelopePhasesHi
   DH adsrOff-1,adsrRelease-1,adsrSustain-1,adsrDecay-1,adsrAttack-1,adsrInit-1

   
adsrInit:   lda #$00         ;initialise amplitude, counter      
   sta envelopeAmp
   sta envelopeCounter
   dec envelopePhase      ;then move to Attack phase
            ;drop through

adsrAttack:   lda attack         ;if Attack = 0, set max amp and move to Decay
   bne @a
   lda #$0F
   sta envelopeAmp
   dec envelopePhase
   rts
   
@a   clc         ;otherwise, add Attack rate to counter
   adc envelopeCounter
   sta envelopeCounter
   lda envelopeAmp      ;if counter overflows, carry is set and the ADC #$00
   adc #$00         ;will increment amplitude
   cmp #$10         ;exceeded max (0F)?
   bcc @b
   dec envelopePhase      ;yes, move to Decay      
   rts
@b   sta envelopeAmp      ;no, store new amplitude value
   rts


adsrDecay:   lda decay         ;if Decay = 0, move to Sustain phase
   bne @a
   dec envelopePhase
   rts
   
@a   clc         ;otherwise, add Decay speed to counter
   adc envelopeCounter
   sta envelopeCounter      ;if counter overflow, carry is set
   ror a         ;this time we need to subtract from amplitude
   eor #$80         ;so use ROR A to push carry into bit 7
   asl a         ;invert bit 7 and push back into carry
   lda envelopeAmp      ;so that SBC #$00 will subtract 1 if carry set after overflow
   sbc #$00
   cmp sustain         ;reached sustain level (or 0)?
   bmi @b
   bcc @b
   sta envelopeAmp      ;no, store new amplitude
   rts
@b   dec envelopePhase      ;yes, move to Sustain phase
   lda #$00         ;and zero counter because it will be indeterminate at this point
   sta envelopeCounter
   rts
   
adsrSustain:         
   lda gateTime      ;if Gate Time = 0, sustain forever. In practicality, you'd
            ;only use 0 gate time if you had a command to force the envelope
            ;into the release phase, as in a MIDI Key Off command
   beq @a         
   inc envelopeCounter      ;otherwise, increment counter until >= Gate Time
   cmp envelopeCounter
   bcs @a
   dec envelopePhase      ;the move to Release Phase
@a   rts
      
adsrRelease:
   lda decay         ;add Release speed to counter
   clc
   adc envelopeCounter
   sta envelopeCounter
   ror a         ;same trick as Decay, invert the carry and do a SBC #$00
   eor #$80
   asl a
   lda envelopeAmp      
   sbc #$00
   bmi @a         ;subtract 1 from amplitude until >=$00         
   sta envelopeAmp
   rts
@a   dec envelopePhase      ;move to Off phase, envelope done
   rts
      
adsrOff:
   lda #$00         ;could replace with just "STY envelopAmp" becase Y=0 at this point
   sta envelopeAmp
   rts