Amiga Machine Code Letter VIII - Wavetable Synthesis
We have reached Letter VIII of the Amiga Machine Code course. As always, make sure to read the letter, since I won’t go through all the details.
In this post we are going to take a look at the mc0802 program. It plays a simple melody and then exits. That’s all there is to it, yet it’s kind of interesting in it’s simplicity.
As mentioned in the previous post, the Amiga sound system revolves around samples, played through it’s four PCM audio channels. This is a somewhat dramatic departure from the old way of generating sound, through wave generators.
Even though samples are the heart of the Amiga sound system, it can quite easily produce sound as if it used wave generators. To achieve this, you simply have to use a single-cycled sample wave in your music. This technique is also called wavetable synthesis.
The sample used in the mc0802 program, reveals a single-cycled sine wave defined by 16 data points. By manipulating the audio period AUDxPER all kinds of musical notes can be produced.
sample: dc.b 0,40,90,110,127,110,90,40,0,-40,-90,-110,-127,-110,-90,-40
Here’s a recording of the mc0802 program sound output:
The mc0802 program is written below, with my comments added. The wait routine will be explained in further detail below.
move.w #$0001,$dff096 ; DMACON disable audio channel 0 lea.l sample,a1 ; move sample address into a1 move.l a1,$dff0a0 ; AUD0LCH/AUD0LCL set audio channel 0 location to sample address move.w #8,$dff0a4 ; AUD0LEN set audio channel 0 length to 48452 words move.w #0,$dff0a6 ; AUD0PER set audio channel 0 period to 700 clocks (less is faster) move.w #0,$dff0a8 ; AUD0VOL set audio channel 0 volume to 0 move.w #$8001,$dff096 ; DMACON enable audio channel 0 lea.l music,a1 ; move music address into a1 mainloop: ; begin mainloop bsr wait ; branch to subroutine wait move.w (a1)+,d1 ; move value pointed to by a1 into d1 and increment a1 (word) move.w d1,$dff0a6 ; set AUD0PER to d1 move.w (a1)+,d2 ; move value pointed to by a1 into d2 and increment a1 (word) move.w d2,$dff0a8 ; set AUD0VOL to d2 cmp.w #0,d1 ; compare 0 with value in d1 bne mainloop ; if d1 != 0 goto mainloop cmp.w #0,d2 ; compare 0 with value in d2 bne mainloop ; if d2 != 0 goto mainloop move.w #$0001,$dff096 ; DMACON disable audio channel 0 rts ; return from subroutine (exit program) wait: ; wait subroutine - waits 5/50th of second moveq #4,d1 ; set wait counter to 4 wait2: ; wait subroutine - waits 1/50th of a second move.l $dff004,d0 ; read VPOSR and VHPOSR into d0 as one long word asr.l #8,d0 ; algorithmic shift right d0 8 bits and.l #$1ff,d0 ; add mask - preserve 9 LSB cmp.w #200,d0 ; check if we reached line 200 bne wait2 ; if not goto wait wait3: ; second wait - part of the wait subroutine move.l $dff004,d0 ; read VPOSR and VHPOSR into d0 as one long word asr.l #8,d0 ; algorithmic shift right d0 8 bits andi.l #$1ff,d0 ; add mask - preserve 9 LSB cmp.w #201,d0 ; check if we reached line 201 bne wait3 ; if not goto wait2 dbra d1,wait2 ; if wait counter > -1 goto wait2 rts ; return from wait subroutine sample: ; sample of a sine wave defined by 16 values dc.b 0,40,90,110,127,110,90,40,0,-40,-90,-110,-127,-110,-90,-40 music: ; pairs of period and volume - wait 1/10th second between pairs dc.w 428,64,428,64 ; C2, C2 at max volume dc.w 428,0 ; C2 at min volume dc.w 381,64,381,64 ; D2, D2 at max volume dc.w 381,0 ; D2 at min volume dc.w 339,64,339,64 ; E2, E2 at max volume dc.w 339,0 ; E2 at min volume dc.w 320,64,320,64 ; F2, F2 at max volume dc.w 320,0 ; F2 at min volume dc.w 285,64,285,64 ; G2, G2 at max volume dc.w 285,0 ; G2 at min volume dc.w 254,64,254,64 ; A2, A2 at max volume dc.w 254,0 ; A2 at min volume dc.w 226,64,226,64 ; H2, H2 at max volume dc.w 226,0 ; H2 at min volume dc.w 214,64,214,64,214,64 ; C3, C3, C3 at max volume dc.w 214,0,214,0,214,0 ; C3, C3, C3 at min volume dc.w 214,64 ; C3 at max volume dc.w 226,64 ; H2 at max volume dc.w 254,64 ; A2 at max volume dc.w 285,64 ; G2 at max volume dc.w 320,64 ; F2 at max volume dc.w 339,64 ; E2 at max volume dc.w 381,64 ; D2 at max volume dc.w 428,64,428,64,428,64 ; C2, C2, C2 at max volume dc.w 428,0,428,0,428,0 ; C2, C2, C2 at min volume dc.w 856,64,856,64,856,64 ; C1, C1, C1 at max volume dc.w 0,0 ; end of music is set by the zero pair
The Amiga period values, typically valued somewhere between 120 and 800, count Paula ticks. For instance, a period value of 400 means that Paula waits 400 ticks holding the output constant, and then changes to the next sample.
If you, like me, do not have access to Amiga 500 hardware, then try to disable sound interpolation in WinUAE. The signal will look like a stepwise single cycle sine wave, that was defined in the mc0802 program by 16 values.
sample: dc.b 0,40,90,110,127,110,90,40,0,-40,-90,-110,-127,-110,-90,-40
The unfiltered output from Paula, is much more hard edged, sounding almost unpleasently metalic, which is a byproduct of the stepwise signal. This distortion is called aliasing, and we can hear it in the sample below. The picture was taken using Audacity - an open source audio editor.
Paula unfiltered output (emulated)
When the signal leaves the Paula chip, it goes through a filter, which removes the aliasing, producing a more pleasent sound.
Emulated Amiga 500 sound as played by WinUAE
The filter is a low-pass filter, that is used on all four audio channels, and is placed on the Amiga 500 motherboard. Here’s a picture from Polynomial.com.
This “hardcoded” filter is not dynamic, and that engineering decision is critized by Antti S. Lankila, who also mentions that some Atari ST models had a better solution.
We have seen the wait rutine before, but never really looked at it more closely. One of the things, that at first can seem confusing, is that when we read VPOSR we are actually also reading VHPOSR by moving a long word into d0.
After moving the two registers into d0, we shift them 8 bit right, and then use an and to mask out all but the first 9 bits. The value in d0 now corresponds to the vertical position of the screens electron beam, which we then compare with a given scan line number.
The reason that we wait for scan line 200 and then 201, is that the assembled wait routine is fast enough to run multiple times on the same scan line. By waiting for both scan lines, we can call wait multiple times and still get the timing right. Roughly 1/50th of a second, which corresponds to the PAL refresh rate of 50 Hz.
I have made a little test program, that counts how many times we can run the wait loop, between scan lines 200 and 201. The loop count is stored in d1, and on an emulated Amiga we can run the loop 5 to 6 times, until scan line 201 is reached.
start: clr.l d1 ; clear d1 wait1: ; wait subroutine - waits 1/50th of a second move.l $dff004,d0 ; read VPOSR and VHPOSR into d0 as one long word asr.l #8,d0 ; algorithmic shift right d0 8 bits and.l #$1ff,d0 ; add mask - preserve 9 LSB cmp.w #200,d0 ; check if we reached line 200 bne wait1 ; if not goto wait wait2: ; second wait - part of the wait subroutine addq.l #1,d1 ; increment d1 by 1 move.l $dff004,d0 ; read VPOSR and VHPOSR into d0 as one long word asr.l #8,d0 ; algorithmic shift right d0 8 bits andi.l #$1ff,d0 ; add mask - preserve 9 LSB cmp.w #201,d0 ; check if we reached line 201 bne wait2 ; if not goto wait2 rts ; return from wait subroutine
The wait routine can be made faster. A variation of the wait routine, saves an instruction by not using a right shift. Take a look at it, it’s a simple trick 😃.
It’s hard to imagine just how fast the Amiga is, when it can perform 5 iterations of the wait loop during the drawing of one scan line. The following video illustrates how little time it takes for a CRT monitor refresh a single line.
The Amiga 500 for PAL is only clocked to 7.09379 MHz, and it is able to do several calculations per scan line. That is pretty fast, but nothing compared to modern computers. It’s really hard to fathom, how fast computers are today.
Previous post: Amiga Machine Code Letter VIII - Audio.
Next post: Amiga Machine Code Letter IX - Interrupts.