Programming:Tutorial - Understanding the fundamentals of BASIC SOUND and the Firmware SOUND QUEUE
Understanding the fundamentals of BASIC SOUND and Firmware's SOUND QUEUE
I've written this short guide for myself and for others to help understand the fundamental processes which are applied when translating a BASIC Sound command into a Firmware Sound Queue. My intention with this is to only to outline the simple processes of SOUND to get it working via the Firmware.
To put this all into place, it's good to know how the BASIC SOUND statement works:
SOUND channel status, tone period, duration, volume, volume envelope, tone envelope, noise period
- Channel Status - can go quite in depth, the main values when Sending Sound to the 3 available Channels are 1,2,4.
- Tone Period - Is pitch of the Sound
- Duration - How Long it Lasts. If you don't specify one in BASIC it will default to 20.
- Volume - 0 as quiet as a Church Mouse, 15 being the loudest (it won't blast a hole though your windows though). If none is specified BASIC will default the volume to 12.
- Volume Envelope - This works in conjunction with the Envelope Volume (ENV) statement which I'm not looking at for the moment, so 0 represents none or even a comma can be used to bypass (personally I like to see a value in that spot - so '0' is nice to see!)
- Tone Envelope - Like Volume Envelope it works in conjunction with Envelope Tone (ENT), which I'm not looking at either, like Volume Envelope the same process can be used for bypass to use the next paramater.
- Noise - this produces a range of white noise which can vary from 0 to 31.
When using the Firmware to make sound the Sound Queue appears to be the equivalent of SOUND in BASIC which is located at &BCAA. It is initated with a series of DATA for producing the effect in the "HL" register pair.
These parameters which HL points to virtually represents the parameters which are used in the BASIC SOUND statement, with the exception that their arranged differently, so the following is how the Firmware Guide shows it:
- Channel Status Byte,
- Volume Envelope,
- Tone Envelope,
- Tone Period (1),
- Tone Period (2),
- Noise Period,
- Duration of Sound (1),
- Duration of Sound (2).
Sample Sound Example 1
To do something like this:
using the firmware could look like this:
org &4000 ld hl,sdata call &bcaa ret .sdata defb 1,0,0,142,0,0,12,208,07
Here's an explaination of that Sound Data:
- 1 : Represents the Channel
- 0,0 : These are Envelope Tone and Envelope Volume paramaters - 0 is used which represents none since I'm not using these at the moment.
- 142 : Represents Tone Period, this paramater is the Lower 8bit since the value is below 255, this position is filled and the next paramater is 0.
- 0 : Represents the upper value for the Tone Period, because tone periods can range upto 4095, this second paramater allows a value upto 15 - it's a 4bit value so largest allowed value is "1111" in binary or 15 decimal or F in hexidecimal.
- 0 : This handles noise frequency, which I think is the last paramater in the Sound command in BASIC which refers to the "noise period", haven't gone this far yet so haven't used it, since there is none 0 is specified for none
- 12 : This is the tricky one - this is used for the Volume which follows the "duration" in the Sound command. The reason I specified 12 is because this is the default value used in BASIC when no Volume has been specified - as in this case.
- 208, 07 : These last 2 paramaters deal with the duration. The tricky bit is explaining how I came up with 208,07 from what is 2000 as used in BASIC, they are indeed the same though. Like the Tone Period, this is a two paramater job which deals in an upper 8bit value and a lower 8bit value (notice their both 8bit). The best way to describe it is to convert 2000 into Hexidecimal. 2000 in Hexidecimal is &7D0. So now what I can do is literally take the &D0 - in decimal that is 208 and this value goes into the Lower bit of the Duration, and 7 in Hexidecimal is 7 in Decimal and that becomes the High bit for the Duration.
One last note to point out with the 2 parameter data values is they are arranged from Lowest Byte First, High Byte Last (as this is normally how it would be applied in assembly).
Sample Sound Example 2
What's interesting though is when I try to do something like this:
for s=15 to 0 step -1:sound 1,0,3,s,,,31:next
Studying that I've transformed it to this which does the same thing (in BASIC)
for s=15 to 0 step -1:sound 1,0,3,s,0,0,31:next
Again like the first example it's using the 'A Channel' which is represented with the first value '1'. Followed by '0' which is the tone period, which can allow upto a 16bit value between (0-4095), the next value being '3' represents the duration of the sound which again can be upto a 16bit value. The next value is interesting which represents the volume - in this situation a Loop has been applied to which adjusts this volume which is in variable 's' to range from '15' to '0' - the rest of the sound data will remain constant. The next variables which I inserted '0's into from the inital statement represent the Volume Envelope and Tone Envelope which I'm not using yet, and finally on the end is a value of '31' which represents the noise period.
To be able do something like this one could set themselves to go about it different ways, for instance using the firmware, it could be something simular to what I had earlier with a Loop (from whatever language it maybe), simply calling it and poking new voume values as the loop decrements itself, or it could be strictly using Assembly via an assembly loop like this:
org &4000 .sque equ &bcaa .ffly equ &bd19 ld b,15 .loop ld hl,volume ld (hl),b push bc ld hl,sdata call sque pop bc call ffly djnz loop ld b,0 ld hl,sdata call sque ret .sdata defb 1 defb 0 defb 0 defb 0 defb 0 defb 31 .volume defb 0 defb 3 defb 0
In this program I've setup the Volume to represent the 'B' register which, on entry of the loop gets poked into area labelled 'volume' below. The Sound Data ("sdata") goes into the HL register and then the Sound Queue is called to play this sound. In this situation I've had to 'push' and 'pop' the 'bc' register as it is critical 'B' isn't corrupt at all, so the loop can proceed without interferance. As suggested to myself on the forum boards a "Frame Flyback" is been used, during initial tests between the BASIC SOUND and using a simular program to the one able to use the Sound Queue with, it was clear the Sound was noticibly different. By applying a Frame Flyback, this has simply turned this Sound Routine into what has been produced using the BASIC SOUND statement. The loop is a simple Decrement the 'B' register by 1 and thw whole process starts again with the new value of 'B' been poked into label 'volume'.
As you can see the data has lots of '0s' in it, here's the BASIC SOUND statement again as a refresher:
for s=15 to 1 step -1:sound 1,0,3,s,0,0,31:next
At the top of our ".sdata" is a value of '1' which represents the channel - Channel A to play a sound out of. The Next two 0s below that represent the Volume & Tone Envelopes respectively which are the two 0s between the variable s and the '31' in the BASIC SOUND statement. The Next two 0s in the Assembly routine is the tone period, which is the '0' between the 1 and 3 in the BASIC SOUND statement, like I said earlier - there's two in the Assembly cause it can represent a 16 bit value (in other words a number larger than 255) which needs 2 memory allocations for it to happen! The next value "31" refers to the Noise period - which is the last parameter in the BASIC SOUND, this follows onto the Volume, anything from 0-15 is valid in that slot and I could personally have anything there in that range since I'm poking the value in, this follows onto the value '3' which is the Duration of the Sound. Again it can handle 16bit values, so it has a value following that with a 0 which is the high byte. Because the value in BASICs SOUND is only a small one, only the low byte of the two is used!
Just going on what I was saying earlier about going about doing a method like this a number of different ways, indeed one can simply have a routine like this:
org &4000 .sque equ &bcaa ld hl,sdata call sque ret .sdata defb 1 defb 0 defb 0 defb 0 defb 0 defb 31 .volume defb 0 defb 3 defb 0
To then use it with another language something like this could be done, in BASIC it would look like this:
for s=15 to 0 step -1:poke &400d,s:call &4000:call &bd19:next
In that case you would have compiled your assembly routine, known where the address of the volume resides and simply poke a value to it, following that with the routine itself and a Frame Flyback to generate the same effect as if it would have come from the SOUND in basic. The advantage to doing this is perhaps clearer when using other Languages which don't have their own SOUND routines, though can access the Firmware Routines in order to generate the effects using that approach.
Holding Notes without Frame Flyback
Back in 2011 when I wrote these sound routines which holds the notes with Frame Flyback, I forgot to look into this further as at the time I was somewhat happy with the result. However, I've rectified this by using the appropriate flag to hold the note until it becomes True. This is the resulting program:
org &4000 .sque equ &bcaa ld b,15 .loop ld hl,volume ld (hl),b push bc ld hl,sdata .holdnote call sque jr nc,holdnote pop bc djnz loop ret .sdata defb 1 defb 0 defb 0 defb 0 defb 0 defb 31 .volume defb 0 defb 3 defb 0
So now the Frame Flyback has been removed (CALL &BD19), and a new label called holdnote is used with jr nc,holdnote causing a jump to take place until carry becomes true.
Sample Sound Example 3
My next BASIC example is a simple example of playing a tune, however BASIC is cleverly using math to play the tune:
10 FOR octave=-1 TO 2 20 FOR x=1 TO 7: REM notes per octave 30 READ note 40 SOUND 1,note/2^octave 50 NEXT 60 RESTORE 70 NEXT 80 DATA 426,379,358,319,284,253,239
For this example, I've simplified my Assembly example by working out the values of each tone and rounding the values to their nearest whole values, this is what I ended up with, with some of the value either being rounded up or down depending on it's decimal weight:
852, 758, 716, 638, 568, 506, 478 426, 379, 358, 319, 284, 253, 239 213, 190, 179, 160, 142, 127, 120 107, 95, 90, 80, 71, 63, 60
From that, that information can be used for the Assembly routine, with each note being pick and stored where the other sound data resides. This produces the simplist of Sound tune engines which like BASIC variants, no other operations are carried out until the routine has run its course.
org &8000 ld b,29 .loop ld hl,(adrtune) ;; address of tune ld e,(hl) ;; put contents of inc hl ld d,(hl) ;; tune into DE ld hl,tone ;; address of tone ld (hl),e ;; store tune inc hl ld (hl),d ;; into tone push bc ;; preserve loop value ld hl,sdata ;; address of sound data .holdnote call &bcaa ;; Sound Queue jr nc,holdnote ;; Hold note until carry is true pop bc ;; restore loop value ld hl,(adrtune) ;; increment address of tune inc hl inc hl ld (adrtune),hl ;; to the next tune value djnz loop ;; loop until b=0 ld hl,tune ;; return the tune ld (adrtune),hl ;; back to the start. ret ;; exit routine. .sdata defb 1,0,0 .tone defb 0,0,0,12,20,0 .adrtune defw tune .tune defw 852,758,716,638,568,506,478 defw 426,379,358,319,284,253,239 defw 213,190,179,160,142,127,120 defw 107,95,90,80,71,63,60,0
The only other limiting factor is how big the tune can be, using this approach only 255 notes can be played. The other thing worthy of mentioning regards the BASIC example. The Sound command lacks duration, so in my Assembly exmaple it also lacked duration, however if no duration is used in BASIC, duration defaults to 20. Initially my Assembly was set to 0,0 so my tune played for longer. Unfortunately it's unclear where it goes since I've setup a label ".tone" to point to store the data from tune there. If you follow the bytes from the ".tone" label defb 0,0 = tone, 0 = noise, 12 = volume & finally 20 = duration (low byte) & 0 = duration (high byte).