Programming:Tutorial - Understanding the fundamentals of BASIC SOUND and the Firmware SOUND QUEUE

From CPCWiki - THE Amstrad CPC encyclopedia!
Revision as of 22:29, 6 October 2018 by CPM User (Talk | contribs) (Sample Sound Example 3)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

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,
  • Volume,
  • Duration of Sound (1),
  • Duration of Sound (2).

Sample Sound Example 1

To do something like this:

sound 1,142,2000

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