Page 1 of 3

ADSR Envelope API

Posted: Mon Apr 26, 2021 8:10 pm
by rje


I was looking at Concerto:




 


I like that approach to envelope management: envelopes are configurations that any voice can use.


So I've been thinking about a sound API. 


I've thought about a full-fledged API, but I think just an envelope manager with an interrupt is the right way.


 


VOICE ENVELOPES: $0200 thru $020F.  Which envelope each voice is using, if any.

VOICE STATES: $0210 thru $021F.  (Internal) state variable.  Current state ('A','D','S','R', or 0 for 'done') of voice.  Used by interrupt.

VOICE TARGETS: $0220 thru $022F.  (Internal) state variable.  Target value for voice.  Used by interrupt.

ENVELOPE DATA: $0230 and up.  Envelope configurations.  Four bytes per envelope: one each for Attack, Decay, Sustain, and Release, in jiffies or something.


 

Theoretically there can be a whole bunch of envelope configs.  Concerto (above) has three.  Performance testing can determine what a reasonable number is (somewhere between 1 and 16+).

...and then an interrupt which modifies voice volume based on the envelope it's set at.

So each cycle, the interrupt checks each voice state and the voice's current volume:



  • If the current state is 0, then skip.


  • If the current state is 255, then set the current state to 'A' and set up the temporary variables for this voice.


  • If the current state is 'A':


    • increase volume.  steepness is based on attack value.


    • if current volume == target, update state to 'D'.




  • If the current state is 'D':


    • decrease volume.  steepness is based on decay value.


    • if current volume == target, update state to 'S'.




  • If the current state is 'S':


    • decrease target.  steepness is based on sustain value.


    • if target == 0, update state to 'R'.




  • If the current state is 'R':


    • decrease volume. steepness is based on release value.


    • if current volume == 0, update status to 0.




 


ADSR Envelope API

Posted: Tue Apr 27, 2021 1:04 pm
by ZeroByte

I think sound is going to fall into two broad categories: active generation and raw playback.

My sound “engine” in Flappy Bird is the latter. I decided to take that route after analyzing the sound engine for Wolf3d.

This is the lightest possible implementation and leaves the most CPU resources for the rest of your program. My sound play routine actually uses the same stream format and update function for both FM and PSG.

It depends on data streams which already contain all of the ADSR / pitch bending / PWM type of stuff baked in. This means a tool must produce this stream (or in my case you make it by hand).

Look for me on Discord. I’d like to discuss this with some of the other sound aficionados.


ADSR Envelope API

Posted: Wed Dec 15, 2021 12:02 am
by rje

My really cheap proof of concept ADSR code is here: https://github.com/bobbyjim/x16-c-tools/blob/main/PSG.c

Note that the code that "runs the envelope" is not an interrupt.  So it's a POC, not a real usable thing really.

 

Envelopes use:































typedef struct {

 

long phase; // along the ADSR envelope

 

int attack :16;

 

int decay :16;

 

int sustain :16;

 

int release :16;

 

} Envelope;

 

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.


ADSR Envelope API

Posted: Wed Jan 26, 2022 8:31 am
by kliepatsch

@rje What kind of functionality would you think would be good for such a C library? The questions I am asking myself are


  • would you rather set a MIDI note (library handles conversion to frequency) or a 16-bit frequency directly?


  • What envelope parameters are desired? Do we need the full ADSR functionality, or would it be sufficient to define attack, release and hold duration (the time in between attack phase and release phase, potentially zero)  (in other words, dropping the decay phase)


  • Should you pass the PSG voice index (0-15) during the function call, or should the library automatically choose a voice?


I am thinking that the easiest would be to have a single function to which you pass all required parameters, like frequency, ADSR parameters, waveform. So you can call the function and then simply forget about it.


ADSR Envelope API

Posted: Wed Jan 26, 2022 9:01 am
by kliepatsch

I think the frequency/MIDI question boils down to the applications you have in mind. The MIDI to frequency conversion is a relatively expensive operation. It's only worth it if the intended use of such a library would be to play music. In all other cases (e.g. sound effects), it is both more efficient and less limiting to directly set the frequency.

Dropping the decay phase makes sense IMO, since in most cases, you either want a short sound (decay = release), or a sustained sound or beep (no decay phase, since you directly enter sustain phase).


ADSR Envelope API

Posted: Wed Jan 26, 2022 3:07 pm
by rje


On 1/26/2022 at 2:31 AM, kliepatsch said:




@rje What kind of functionality would you think would be good for such a C library?





...



I am thinking that the easiest would be to have a single function to which you pass all required parameters, like frequency, ADSR parameters, waveform. So you can call the function and then simply forget about it.



Here's the functions I coded up in my proof-of-concept.


void runVoice( unsigned voiceNumber, Voice* voice );



void runVoiceWithEnvelope( unsigned voiceNumber, Voice* voice );



int getTunedNote( unsigned index );



void bang(unsigned frequency);


Each voice has a dedicated envelope, so runVoiceWithEnvelope() can figure out the envelope's address.

 


ADSR Envelope API

Posted: Wed Jan 26, 2022 3:10 pm
by rje


On 1/26/2022 at 3:01 AM, kliepatsch said:




Dropping the decay phase makes sense IMO, since in most cases, you either want a short sound (decay = release), or a sustained sound or beep (no decay phase, since you directly enter sustain phase).



I can see that.  And that's a good simplification for an interrupt-driven envelope manager.  Thanks.


ADSR Envelope API

Posted: Wed Jan 26, 2022 10:09 pm
by BruceMcF


On 1/26/2022 at 3:31 AM, kliepatsch said:




@rje What kind of functionality would you think would be good for such a C library? The questions I am asking myself are




  • would you rather set a MIDI note (library handles conversion to frequency) or a 16-bit frequency directly?


  • What envelope parameters are desired? Do we need the full ADSR functionality, or would it be sufficient to define attack, release and hold duration (the time in between attack phase and release phase, potentially zero)  (in other words, dropping the decay phase)


  • Should you pass the PSG voice index (0-15) during the function call, or should the library automatically choose a voice?




I am thinking that the easiest would be to have a single function to which you pass all required parameters, like frequency, ADSR parameters, waveform. So you can call the function and then simply forget about it.



Decay is if you want pseudo horns, woodwinds, string instruments (including piano) or percussion, though if you wanted to simplify decay, you would go a long way with a decay of 1/4, 1/2 or 3/4 the attack, at the same rate as the attack, so attack level, attack duration, decay type (of four, two bits, also defines sustain level of 100%, 75%, 50% or 25% of attack level), sustain duration, decay duration.

You could, eg, define 7 patches, with each voice having a two-byte ADSR index, the top three bits indicate the selected patch (or ADSR off) and the bottom 13 bits indicating progress through the envelope.


ADSR Envelope API

Posted: Thu Jan 27, 2022 6:43 am
by TomXP411


On 1/26/2022 at 12:31 AM, kliepatsch said:




@rje What kind of functionality would you think would be good for such a C library? The questions I am asking myself are




  • would you rather set a MIDI note (library handles conversion to frequency) or a 16-bit frequency directly?


  • What envelope parameters are desired? Do we need the full ADSR functionality, or would it be sufficient to define attack, release and hold duration (the time in between attack phase and release phase, potentially zero)  (in other words, dropping the decay phase)


  • Should you pass the PSG voice index (0-15) during the function call, or should the library automatically choose a voice?




I am thinking that the easiest would be to have a single function to which you pass all required parameters, like frequency, ADSR parameters, waveform. So you can call the function and then simply forget about it.



I think both should note and frequency should be an option. Call the "Play Note" function to pick a frequency based on the note. Call the "Play Frequency" function to play a specific frequency.  If you implement a lookup, then allowing the composer to play a frequency direct is free - you already wrote the routine, so the composer just hits a different entry point. 

There are going to be times when the programmer will want an arbitrary frequency: sound effects, pitch bends, etc. But when playing music, they will just want a note on the scale and don't care what the actual frequency is. You could even enhance this with a global tuning and pitch bend options, making the CX16 even more useful in music environments. 


Quote




What envelope parameters are desired?



You need not just ADSR, but expression (the "dynamics" of a note or passage) is needed for dynamics changes. 

Thinking about ADSR, it's the least common denominator that describes all simple instruments. All instruments have an attack and all have a release. Whether they have a decay varies by instrument, but all instruments have either a decay, sustain, or both. And yes, the decay rate is different than the release rate - especially for stringed instruments. 

image.png.2501cc2f372c2adcb9c615209d497439.png

And we also need to consider crescendos and other dynamics changes. The "Expression" controller affects not only the final volume of the note, but may affect the filter and ADSR curve. This is necessary to allow for dynamics changes, since volume is really supposed to be used for mixing, not dynamics changes. 


Quote




Should you pass the PSG voice index (0-15) during the function call, or should the library automatically choose a voice?



One channel, one voice. But add the ability to quickly change voice parameters, so composers can switch voices when needed. It might be worth trying to dynamically allocate voices, but remember that every byte of overhead for your player is a byte that the application doesn't get. I think this is a case where space is more important than functionality. 

 


ADSR Envelope API

Posted: Thu Jan 27, 2022 7:29 am
by kliepatsch

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 ...