First things first though. My initial kickstart for this was looking at some wavetables and wondering if it would be possible to use them on the Speccy. The tables I'm going to use are essentially 256-byte sets of 8-bit samples which have been reduced to 4-bits, giving a possible range of 0 to 15.
To play back the wavetable there are a few ways to do it. The one I decided to go with and used in a much earlier project years ago called "PC Tracker" was to use fixed-point maths to step through the table according to the playback rate needed.
In this case I'm using 8.8-bit fixed point increments which is implemented as follows:
This way an increment value of $0100 corresponds to a step rate of 1 byte per cycle, $0080 is a half-byte and $0001 is 1/256 of a byte. Like wise, $0200 is a step rate of 2 bytes per cycle, $0280 is 2.5 bytes per cycle and so on. Doing it this way keeps everything simple and (in theory) fast and easy to implement.
To calculate the table of increment values that correspond to the music scale we only need to know a few things:
So, if your wavetable used 11025Hz as the sample frequency for middle C, it needs to be played back at 22050Hz to play C of the next octave up. The get the sample rate of C# you multiply 11025 by 1.05964 (which is roughly the twelfth root of 2) which gives 11680Hz. For D, you multiply that again and keep successively multiplying. You'll find that after you do this 12 times you'll reach 22050 - Well, not quite. Due to cumulative errors in the multiplication it'll be off a bit, but there is a more accurate way of calculating it which I'll do later when I'm happy with the code.
I've chosen to represent the notes as n+octave+note+[sharp], i.e. n1c, n1cs, n1d, n1ds, etc.
snd_v1.asm is pretty much the first go of this and at least it doesn't crash. I'm using Zeus to assemble these and Spectaculator to run them, but I'll be trying them on the real thing soon enough as the emulated sound is not quite the same as driving a real speaker.
To generate the pulse I'm adding the values from the two channels (yes, I decided to be ambitious and start with two channels straight away), reducing them to 3 bits and then using DJNZ to count a loop with the speaker on, then a loop using the inverted value with the speaker off. This means that the cycle time of the pulses is almost exactly the same every time (it'll vary because of the differnt timings between DJNZ loop and DJNZ no-loop.) It's not the best way of doing it as it means the speaker is off for most of each cycle, but it'll do until I work out the theory.
As can be seen from the source I've made up three wave tables for a square wave, sine-wave and a rising sawtooth wave. They're aligned on 256-byte boundaries so that they can be addressed simply by setting the high byte of HL to the start boundary of the table. The high byte of the byte position (ch1pos and ch2pos here) is used as the low byte and a simple ld a,(hl) is then used to pull the value out of the table.
In later revisions I'll be working put the machine cycle timings for the instructions to get the pulse rate as high as possible. As the Spectrum only runs at 3.5MHz with most opcodes taking between 4 and 8 cycles to run, the rate is likely to be in the audible range (I think v1 works out to about 10Khz) so the faster the code runs the better the chances are that it won't mask the audio signal we're trying to reproduce.