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
    +1 for reminding me of trackers. – Todd Wilcox May 9 '16 at 21:05

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

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.

| improve this answer | |
  • I wonder why the code xors the results with 128 rather than simply subtracting 64 from the samples in RAM? – supercat May 9 '16 at 23:26
  • XOR with 128 is a fast way to convert from 2-complement to unsigned binary. It's the equivalent of adding 128 to the result. – mcleod_ideafix May 9 '16 at 23:59
  • Yeah, but I subtracting 64 from the audio source data would have the same effect, wouldn't it? – supercat May 10 '16 at 14:25
  • 1
    eor.b and add.b with a register actually have the exact same cycle timing on a 68000 CPU. – tofro Apr 14 '17 at 7:43
  • I'm still not understanding how different pitches are compensated for, i.e. how does it work when sample A w/ requested pitch PA is mixed with sample B at pitch PB? This is much more complex than simply multiplying according to their volume, isn't it? – DrMickeyLauer Feb 25 '19 at 9:00

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)
| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.