Amiga Machine Code - Letter VIII

Amiga Machine Code Letter VIII - Wavetable Synthesis

Amiga Machine Code - Letter VIII

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

Interpolation

According to a (very) detailed description of the Amiga sound by Antti S. Lankila, the sound chip - Paula - does not do any interpolation at all. Paula only generates pulse waves.

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

sample unfiltered

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.

sample filtered

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.

Amiga audio filter

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.

Wait subrutine

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.

vposrVhposr

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.

Avatar
Mark Wrobel
Team Lead, developer and mortgage expert