ADSR Envelope API

Chat about anything CX16 related that doesn't fit elsewhere
User avatar
kliepatsch
Posts: 247
Joined: Thu Oct 08, 2020 9:54 pm

ADSR Envelope API

Post by kliepatsch »


I see your point, leaving decay away limits the musical use. So the question boils down to whether the intended use includes music or only simple sound effects.

TomXP411
Posts: 1785
Joined: Tue May 19, 2020 8:49 pm

ADSR Envelope API

Post by TomXP411 »



On 1/26/2022 at 11:29 PM, kliepatsch said:




currently you cannot make a sound with a single line of code (or two). I may be wrong



Yeah, the Commander definitely needs commands for managing sound. BASIC 7 had a PLAY command, which allowed for not only playing music, but changing audio parameters on the fly. And it has a separate command for playing beeps and sound effects. Both were useful, and the PLAY command was especially nice since it had a sequencer (you could issue a PLAY command and the program would continue immediately, playing the notes in the background.)

The simplest API I can think of looks like this...

adsr_load(string filename, byte program)

would load a sound into a program slot in memory. Then later, you could do: 

adsr_set_program(byte channel, byte program)

to set up the channel, then 

adsr_play_sound(byte channel, int frequency, int duration)

to play an arbitrary tone or 

adsr_play_note(byte channel, byte note)

to play a note using a MIDI note number. The note would continue to play until the program sends

adsr_play_stop(byte channel)

which would stop playback of either sound or note. 

Obviously, there would be a lot more to the API, to allow for editing the parameters of the synthesizer, but those would be the commands 

Those could be implemented as a BASIC extension or included in a C library. Either way, the idea is that all of the setup for an instrument is encapsulated in the set_program function, so programmers can get to playing sounds very quickly. 

 

rje
Posts: 1263
Joined: Mon Apr 27, 2020 10:00 pm
Location: Dallas Area

ADSR Envelope API

Post by rje »



On 1/27/2022 at 1:29 AM, kliepatsch said:




Well, rje has been talking about an ADSR manager for a while now, and I was interested in his wishes specifically. My impression was that he doesn't want a second Concerto, but rather address a simple problem: currently you cannot make a sound with a single line of code (or two). I may be wrong ...



I have code that can set up a voice -- that's the easy part.  What I lack is the thing that "curates" the sound through an envelope in a "fire and forget" manner.

The ADSR manager is the piece that requires working off of an interrupt to "curate" a played note, so to speak.  In my mind, it would be handled by assembly code, since as an interrupt process it should be as efficient as possible.

Envelope_Manager:     ; voice is in X?

    load status, indexed by X

   ...dispatch based on state...

Attack: 

   increase volume by a_ratio,x

   increment status if it's at max_vol and fall through

   else rts

Decay:

   decrement volume by b_ratio,x

   increment status if it's at decay_vol and fall through

   else rts

Sustain:

   increment status if sustain is done and fall through

   else rts

Release:

   decrement volume by r_ratio,x

   turn off voice and mark done if it's at zero_volume



   rts

   

Ed Minchau
Posts: 503
Joined: Sat Jul 11, 2020 3:30 pm

ADSR Envelope API

Post by Ed Minchau »



On 1/28/2022 at 10:06 AM, rje said:




I have code that can set up a voice -- that's the easy part.  What I lack is the thing that "curates" the sound through an envelope in a "fire and forget" manner.



The ADSR manager is the piece that requires working off of an interrupt to "curate" a played note, so to speak.  In my mind, it would be handled by assembly code, since as an interrupt process it should be as efficient as possible.



Envelope_Manager:     ; voice is in X?



    load status, indexed by X

   ...dispatch based on state...



Attack: 



   increase volume by a_ratio,x

   increment status if it's at max_vol and fall through

   else rts



Decay:



   decrement volume by b_ratio,x

   increment status if it's at decay_vol and fall through

   else rts



Sustain:



   increment status if sustain is done and fall through

   else rts



Release:



   decrement volume by r_ratio,x

   turn off voice and mark done if it's at zero_volume



   rts

   



I see it as being two parts: the part in the custom IRQ like you describe, plus setting a flag bit. Then your main program looks for that flag, and if it's set sends all the voice volume data to VERA'S PSG registers and clears the flag.

BruceMcF
Posts: 1336
Joined: Fri Jul 03, 2020 4:27 am

ADSR Envelope API

Post by BruceMcF »



On 12/14/2021 at 7:02 PM, rje said:




... They're linear timeouts.  So, not as versatile as the C64's envelopes.  Still, 2^16 jiffies is... uh, I think it's 18 minutes.



65536 jiffies x 1 second / 60 jiffies x 1 minute / 60 seconds = 65536/3600 = 18 min.



The maximum SID length is roughly (depending PAL or NTSC clock) 24 seconds (for both delay and release), which is 1,440 jiffies. For attack it's 8 seconds which is 480 jiffies.

So for linear jiffies spanning SID's maximum durations, you need 9, 11 and 11 bits (being linear, they don't have precise matches for each of the 0 to 15 values in a SID ADSR setting).

Then you need two levels: maximum volume (which with the attack length defines the attack increment), and sustain volume as a fraction of maximum volume (which combined with maximum volume and decay length defines the decay increment, and which with maximum volume and release length defines the release increment).

Sustain volume can easily be a four bit value which is a fraction relative to maximum volume, as in the SID.

So if maximum volume is a note setting (along with waveform and frequency), the linear ADR in jiffies covering the maximum lengths of the SID ADSR values combined with Sustain in "n+1" sixteenths requires 35 bits, which packs to 5 bytes, leaving up to five bits for additional information.

TomXP411
Posts: 1785
Joined: Tue May 19, 2020 8:49 pm

ADSR Envelope API

Post by TomXP411 »



On 1/28/2022 at 9:06 AM, rje said:




I have code that can set up a voice -- that's the easy part.  What I lack is the thing that "curates" the sound through an envelope in a "fire and forget" manner.



The ADSR manager is the piece that requires working off of an interrupt to "curate" a played note, so to speak.  In my mind, it would be handled by assembly code, since as an interrupt process it should be as efficient as possible.



Yes, you need every cycle you can get, and honestly, this isn't hard to do. It's all simple integer math and loops. 

So obvously you need a Stage flag, along with the Attack Rate, Decay Rate, Sustain Level, and Release rate. You also are going to need "Current Level", "Channel Volume", and "Expression Volume" parameters. 

I'm making some assumptions:


  • Stage is a bit mask


  • Attack, Decay, and Release are byte values. 


    • 0=slowest, 255=fastest. Max time is 4.25 seconds (or 1092 seconds with 16-bit counters)


    • Value is change per tick.  Larger number = faster attack/decay/release




  • Sustain and Current Level are byte values. 0=silent, 255=loudest


  • Channel Volume and Expression are inverse byte value. So 0=loudest, 255=silent


Stage:

0: Not active

1: Note start

2: Attack

4: Decay

8: Sustain

16: Release

So you can check the current stage with an LSR/BCS cycle, like this: 



LSR

BCS StartNote

LSR

BCS Attack

LSR 

BCS Decay

LSR 

BCS Sustain

LSR

BCS Release

JMP SetNoteVolume


  • StartNote would initialize the sound generator to the right frequency and set the volume to the starting level.


  • Attack would increment the volume by AttackRate each tick. So if AttackRate is 1, it takes 4.25 seconds to reach full volume. So higher values attack faster, with 255 being the shortest attack rate (one tick.) 


    • Add Level and AttackRate 


    • Is Volume = 255 (or 65535)


      • Yes, set Stage to Decay




    • Jump to SetNoteVolume




  • Decay subtracts DecayRate from volume


    • If volume <= SustainLevel


      • Set volume to SustainLevel


      • Set Stage to Sustain




    • Jumps to SetNoteVolume.




  • Sustain is just a test against "note on". When this changes to "note off", advance Stage to Release. 


  • Release subtracts ReleaseRate.


    • If Volume <= 0 set Stage to 0




And SetNoteVolume is where you set the final output levels, after accounting for channel volume and expression volume. I don't know how the PSG handles channel volume, but the formula is: Output = Oscillator_Level * Expression * Volume, where all values are real numbers between 0 and 1, inclusive. 

In other words, if your oscillator level is currently 0.8, your volume is 0.75, and your expression is 0.75, the final output level is actually 0.45. 

Obviously, you don't want to do floating point math to make this work, so I suggest storing channel volume and expression as attenuation levels. Attenuation is a fancy word for "subtraction", so your formula would be Output= Oscillator_Level - Expression - Volume

So Channel Volume and Expression would be loudest at 0 and quiet at 255. 

This sounds like a lot, but each parameter is important:


  • Channel Volume is like the big fader on a mixer. This adjusts the instrument's overall value in a mix. This is the relative volume of one instrument compared to the others, so channel volume is usually fixed for the duration of the passage.


  • Expression is the volume for that note. Expression will change over time as the dynamics of the music change. It will also go up when an instrument needs to be out front for a solo and back down when an instrument needs to blend for harmonies.


  • Program Volume (or patch volume) is the volume for a specific patch. This is based on how loud this sound is, relative to the other patches in your sound bank. As one of the final stages of editing a patch, you'll adjust this so all of your patches have a similar volume level. This means a track won't get louder or quieter when you change instruments. 


Obviously, this could be simplified for simple sound effects. By starting with 0 for both Volume and Expression, the sound engine would play all sounds at max level. Then the application programmer can decide whether to reduce the sounds for mixing audio by setting those values to a non-zero number for mixing and dynamics purposes... or, perhaps, to have loud sounds be closer to the player character and quieter sounds be further away. 

 

BruceMcF
Posts: 1336
Joined: Fri Jul 03, 2020 4:27 am

ADSR Envelope API

Post by BruceMcF »



On 1/28/2022 at 7:36 PM, TomXP411 said:





  • Attack would increment the volume by AttackRate each tick. So if AttackRate is 1, it takes 4.25 seconds to reach full volume. So higher values attack faster, with 255 being the shortest attack rate (one tick.) 


    • Add Level and AttackRate


    • Is Volume < 255? 


      • Yes, set Stage to Decay




    • Jump to SetNoteVolume




  • Decay subtracts DecayRate from volume. It then tests the volume against the SustainLevel, then jumps to SetNoteVolume


  • Sustain is just a test against "note on". When this changes to "note off", advance Stage to Release


  • Release is the same as Decay, except the final level is 0. When level is 0, you can set the stage back to 0. 




I presume that in




  • Quote




     




    • Is Volume < 255? 


      • Yes, set Stage to Decay






     







That either the "<" is an "=" or the if "Yes" is actually an "if no".

Note that linear attack settings can readily be "n+1" by simply using SEC rather than CLC when performing the addition, and then it is "Is volume = 256", which is BCS. Then the slowest attack is "0" rather than "1".

 

TomXP411
Posts: 1785
Joined: Tue May 19, 2020 8:49 pm

ADSR Envelope API

Post by TomXP411 »



On 1/28/2022 at 4:31 PM, BruceMcF said:




Sustain volume can easily be a four bit value which is a fraction relative to maximum volume, as in the SID



The PSG volume is 6 bits, so the sustain should be a minimum of 6 bits. However, to simplify programming, it's probably simpler to start with an 8-bit volume and shift that right 2 bits to get the output volume. 

Yeah, there's enough room for 16-bit math. With my example above, you'd just extend the values to 16 bits by adding 8 bits to the right, then using the high byte for the final volume calculation. 



Assume A has the final level and , after volume attenuation: 

AND $00111111

ORA ChannelMask,Y  ; the top 2 bits of volume register are the stereo channels.

STA [PSG register 2]



 

TomXP411
Posts: 1785
Joined: Tue May 19, 2020 8:49 pm

ADSR Envelope API

Post by TomXP411 »



On 1/28/2022 at 5:19 PM, BruceMcF said:




I presume that in




  •  




That either the "<" is an "=" or the if "Yes" is actually an "if no".



Note that linear attack settings can readily be "n+1" by simply using SEC rather than CLC when performing the addition, and then it is "Is volume = 256", which is BCS. Then the slowest attack is "0" rather than "1".



 



You're right. I still haven't internalized 6502 assembly enough to remember some of the finer points. 

Ed Minchau
Posts: 503
Joined: Sat Jul 11, 2020 3:30 pm

ADSR Envelope API

Post by Ed Minchau »



On 1/28/2022 at 5:36 PM, TomXP411 said:




LSR

BCS StartNote

LSR

BCS Attack

LSR 

BCS Decay

LSR 

BCS Sustain

LSR

BCS Release

JMP SetNoteVolume



Alternatively, suppose the Stage byte is in zero page. I'll use address 7F for this example:

BBS0 7F, StartNote 

BBS1 7F, Attack

BBS2 7F, Decay

BBS3 7F, Sustain

BBS4 7F, Release

JMP SetNoteVolume

 

This saves 6 cycles.

Post Reply