TransWikia.com

How was 8-channel audio achieved on Amiga?

Retrocomputing Asked by SF. on January 21, 2021

Oktalyzer was a Tracker style program for Amiga, that allowed composing and playing music with 8 channels instead of the “hardware-natural” 4.

So far I learned it achieved that by splitting the 4 8-bit channels into 8 4-bit ones. But I still don’t understand how it was done from ‘mathematics’ point of view – it doesn’t seem like you can split a DAC index into two halves and have two waveforms of comparable volume overlaid.

One would range 0-16 in 1-bit increments and the other 0-256 in 16-bit increments, resulting in one being 16 times more quiet than the other. Or you could take every other bit, achieving 16-bit increments on both, but one would still be half as loud as the other, not having access to the most significant bit. How was it done in reality?

2 Answers

According to the very same link you provided:

To perform this feat, Oktalyzer loaded eight channels in memory, mixed them in real time down to four channels, and sent the result to the Amiga sound chip. This was a processor-intensive task which degraded sound quality, but was more than made up for with doubled channels. Oktalyzer could also be run in 4-channel mode to suit more processor-heavy programs.

which makes more sense to me that splitting each channel into two 4-bit channels.

So the process is quite straightforward. During the time between two VBlank interrupts (these occurs normally each 20 ms), the CPU has to take two sample buffers, mix them and write the result to a channel buffer that will be used to feed Paula.

This is quite processor intensive, as for each sample in a 20 ms buffer, the CPU has to calculate what sample to fetch (depending upon the playing frequency), check if it reached the end of sample so it will start at repetition begin. With both samples gathered, each one has to be scaled according to its volume setting, then the CPU has to add them and possibly, apply some compression to avoid clipping while keeping the original dynamic range (adding both sample and then dividing into 2 would result in too quiet sound)

I've just digged into the source code of an early release of Oktalyzer and found this macro: (comments of my own)

GetSample:  macro
        move.b  (a0,d0.w),d4  ;get sample from buffer addressed by a0
        add.b   (a1,d1.w),d4  ;add sample from buffer addressed by a1
        eor.b   d5,d4         ;d5=-128. Fast way to add -128 to result
        move.b  d4,(a5)+      ;store result into output buffer
        swap    d0
        swap    d1
        add.l   d2,d0        ;calculate offset for next sample, buffer a0
        add.l   d3,d1        ;ditto for buffer a1
        swap    d0
        swap    d1
        endm

Which pretty does much of what I've explained (minus the volume scaling operation).

From this source code, I guess that initial buffers pointed by a0 and a1 already contain scaled samples, as there is no code in the macro to check for clipping. I also assume that these original samples are 7 bits unsigned samples. Adding them won't produce clipping, but we will end up with a 8-bit unsiged sample, which is not suitable for Paula, which expects 8-bit signed samples. To get that, we have to substract 128 from the sample value, that's to say, add -128. Adding -128 to a 8-bit number is just toggling its MSb, so d5 is preloaded with -128 and then it is XORed with the calculated sample.

Correct answer by mcleod_ideafix on January 21, 2021

Since I can't comment, I thought I'd add to mcleod_ideafix's answer and explain how pitch is handled with the existing code -- it's using 16.16 fixed point arithmetic. So, if you wanted the loop to increment one address per loop; you'd provide a value of $00010000, but if you wanted to halve the frequency, you'd provice $00008000. What's happening here is that the high word will now only increment every other loop. We then simply SWAP the high and low words to use the high portion in our address offset.

; code enters with d2 and d3 holding the period counters in 16.16 notation
; a0 and a1 point to our sample addresses and d0/d1 should be zero
GetSample:  macro
        move.b  (a0,d0.w),d4  ; get sample from buffer addressed by a0 + d0
        add.b   (a1,d1.w),d4  ; add sample from buffer addressed by a1 + d1
        eor.b   d5,d4         ; d5=-128. Fast way to add -128 to result
        move.b  d4,(a5)+      ; store result into output buffer
        swap    d0            ; restore d0/d1 to normal 16.16 format
        swap    d1            ; (high in high half, low in low half)
        add.l   d2,d0         ; calculate offset for next samples
        add.l   d3,d1         ; 16.16 addition is the same as 32.0
        swap    d0            ; put the high word back into the low half
        swap    d1            ; for each index (this is effectively a /65536)
        endm

Answered by Renee Cousins on January 21, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP