Part 5: Improving the Sound
You've followed this tutorial series and have come up with a little 1-bit sound routine of your own design. Only problem - it still sounds crap - notes are detuned, there are clicks and crackles all over the place, and worse of all, you hear a constant high-pitched whistle over the music. So, it's time to address some of the most common sources of unwanted noise in 1-bit sound routines, and how to deal with them.
Timing Issues
In order to keep the pitch stable, you need to make sure that the timing of your sound routine is as accurate as possible, ie. that each iteration of the sound loop takes exactly the same time to execute.
Let's take a look again at the first example from part 1. We can see that that code is not well timed at all:
soundLoop:
xor a ;4
dec b ;4
jr nz,skip1 ;12/7
ld a,#10 ;7
ld b,c ;4
skip1:
out (#fe),a ;11
xor a ;4
dec d ;4
jr nz,skip2 ;12/7
ld a,#10 ;7
ld d,e ;4
skip2:
out (#fe),a ;11
dec hl ;6
ld a,h \ or l ;8
jr nz,soundLoop ;12
;78/92t
Due to the two jumps, there are four different paths through the core (no jump taken, jump 1 taken, jump 2 taken, both jumps taking), and the sound loop length thus varies up to 12 t-states - that's more than 15% of the sound loop length, and therefore clearly unacceptable. We need to make sure that the sound loop will always take the same amount of time regardless of the code path taken. One possible solution would be to introduce an additional time-wasting jump:
soundLoop:
xor a ;4
dec b ;4
jr nz,wait1 ;12/7----
ld a,#10 ;7
ld b,c ;4
nop ;4-------7+7+4+4=22
skip1:
out (#fe),a ;11
xor a ;4
dec d ;4
jr nz,wait2 ;12/7
ld a,#10 ;7
ld d,e ;4
nop ;4
skip2:
out (#fe),a ;11
dec hl ;6
ld a,h \ or l ;8
jr nz,soundLoop ;12
;100/100t
wait1:
jp skip1 ;12+10=22
wait2:
jp skip2
There are other possibilities, but I'll leave that for another part of this tutorial.
Row Transition Noise
A common moment for unwanted noise to occur is ironically not during the sound loop, but between notes - the moment when you're reading in new data and updating your counters etc. This is called row transition noise.
Row transition noise is very difficult to avoid. Your focus should therefore be on reducing transition noise rather than trying eliminating it. The key to this is to read in data as fast and efficiently as possible. Not much else can be said about this, except: Make sure you optimize your code. For starters, WikiTI has an excellent article on optimizing Z80 code.
Theoretically, there is a way for eliminating transition noise, though in practise very few existing beeper engines use it (Jan Deak's ZX-16 being a notable example). That way is to do parallel computation, ie. read in data while the sound loop is running. Obviously this is not only rather difficult, but also it is usually only feasible on faster machines - on ZX Spectrum, it will most likely slow down your sound loop too much.
Which brings us to another problem...
Discretion Noise
Discretion noise, also known as parasite tone, commonly takes the form of a high-pitched whistling, whining, or hissing. It inevitably occurs when mixing software channels into a 1-bit output and cannot be avoided. It is usually not a big deal when doing PFM, but can be a major hassle with Pulse Interleaving. The solution is to push the parasite tone's frequency above the audible range. In other words, if you hear discretion noise, your sound loop is too slow. As a rule of thumb, on ZX Spectrum (3,5 MHz) your sound loop should not exceed 250 t-states.
Let's take a look at the asm example from part 4 again. At the end of the sound loop, there is a relative jump back to the start (jr nz,soundLoop). A better solution would be to use an absolute jump (jp nz,soundLoop) instead, because an absolute jump always takes 10 t-states, but a relative jump takes 12 if the jump is actually taken, which we assume to be the case here.
Also, leading up to the jump we have
dec ix
ld a,ixh \ or ixl
jr nz,soundLoop
which takes a whopping 38 t-states. It may be a good idea to replace it with
dec ixl
jr nz,soundLoop
dec ixh
jp nz,soundLoop
This will take only 20 t-states except when the first jump is not taken. It will introduce a timing shift every 256 sound loop iterations, but this is usually not a major problem, as it happens at a frequency below audible range.
I'll cover some more tricks for speeding up synthesis in one of the following parts.
IO Contention
This section addresses a problem that is specific to the ZX Spectrum. You can most likely skip this section if you're targetting another platform.
IO Contention is an issue that occurs on all older Spectrum models up to and including the +2. The implication is that in certain circumstances, writing values to the ULA will introduce an additional delay in the program execution. You don't need to understand the full details of this, but if you are curious you can read all about IO contention here.
What's important to know is that delay caused by IO contention affects our sound loop timing. Which is bad, as I've explained above. For sound cores with only one OUT command the solution is rather trivial: You just need to make sure that the number of t-states your sound loop takes is a multiple of 8. For ideal sound in cores with multiple OUTs however, the timing distance between each OUT command must be a multiple of 8. Naturally this is pretty tricky to achieve (and chances are your core will sound ok without observing this), but keep it in mind as a general guideline.
Edit 15-12-01: Added/changed info as suggested by introspec.