ADSR Envelope API
- kliepatsch
- Posts: 247
- Joined: Thu Oct 08, 2020 9:54 pm
ADSR Envelope API
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.
ADSR Envelope API
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.
ADSR Envelope API
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
-
- Posts: 503
- Joined: Sat Jul 11, 2020 3:30 pm
ADSR Envelope API
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.
ADSR Envelope API
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.
ADSR Envelope API
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.
ADSR Envelope API
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".
ADSR Envelope API
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]
ADSR Envelope API
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.
-
- Posts: 503
- Joined: Sat Jul 11, 2020 3:30 pm
ADSR Envelope API
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.