Wednesday 24 May 2023

Fractional sample stepping

For the past few weeks, I've been working on an Amiga module player for DOS, using the Sound Blaster. There are numerous reasons I wanted to do this, one being that I wanted to use sample-based music in my projects, and the other being that I have a long background with music trackers (I've been using OpenMPT to produce music for nearly a decade!). I thought it would be a fun challenge, but I couldn't have done anything without the invaluable FMODDOC.ZIP, which tells you everything you need to know about the format.

I managed to get all the module info read in successfully, but then I hit the stumbling block of pitching samples around. It isn't something I was ever able to pull off successfully, but thanks to the documentation, I found a shockingly simple way of pulling it off! Seeing as it was so simple, I decided to make it part of Blastlib, and as a result, module playing will depend on this library. The actual player isn't written yet, I was too excited to share this first :P

First, we need to figure out the scale factor. This involves dividing the target sample rate by your overall mix rate. Let's say Blastlib's currently mixing sounds at 22050hz, and we want to play a sample at 13400hz (because it's a funny number). We could just divide it, but that'll only give us a whole result. So, we need to use 32-bit precision by using 2 16-bit variables (or if you're using long samples, 32-bit variables): one to hold the whole part, and the other to hold the fractional part. These can be calculated as follows:

  • Whole: Target Rate / Mix Rate
  • Fractional: ((Target Rate % Mix Rate) << 16) / Mix Rate
So our resulting formulae will be:
  • Whole: 13400 / 22050 = 0
  • Fractional: ((13400 % 22050) << 16) / 22050 = 9B92h (word), 9B937953h (dword)

For the fractional part, just use the remainder of the previous division. This is especially important if you're using assembly like me. So just use dx from the first division!

Here's an example in assembly language, interpolated from Blastlib: (assuming eax contains the target rate)

xor edx,edx
mov ebx,mix_rate
div ebx ; sample rate / mix rate
mov dword [sample_scale_whole],eax
mov eax,edx ; put remainder into eax
shl eax,16
mov ebx,mix_rate
div ebx ; ((sample rate % mix rate)<<16)/mix rate
mov dword [sample_scale_frac],eax

Now we have the scale factor, we need to bring in another variable, which is simply a counter. This serves no other purpose than to set the carry, but more on that shortly. We'll refer to this counter as the scale counter. In my mixing routine, I was simply stepping through the sample, byte by byte. If we're scaling a sample, we need to perform some extra steps.

First, add the fractional part to the scale counter. If the number overflows, the carry will be set. Then, we need to add the whole part to the sample position, with carry. What does that mean? Well, let's say the scale counter overflowed. The carry would be set, so we add an extra 1 alongside our whole value.

Here's the source code in assembly language (using 32-bit values):
mov eax,[sample_scale_frac]
add dword [sample_scale_count],eax ; add fractional value to counter
mov eax,[sample_position]
adc eax,[sample_scale_whole] ; add the whole part to the sample position + carry
mov dword [sample_position],eax

Pretty neat trick right? That's how you do it! I've made a varispeed demo using this method; the source is available here, and the executable is available here. It works with any .raw file because it uses streaming. Simply pass the filename as the argument when running the file!

Amendment

When it comes to writing a module player, notes are stored as period values. These values have to be converted to a frequency before you can scale the samples! Use the following formula:

Frequency = 7159091 / (Period * 2)

7159091 is the clock speed of an NTSC Amiga machine, rounded up (7.15mhz). This is important because on an Amiga, sample speed is linked directly to the processor's timing. The period value is how many cycles to wait before grabbing the next sample byte.

So, if we wanted to get the sample rate of a sample playing at period 428 (middle C), we'd do the following:

7159091 / (428 * 2) = 8363hz

No comments:

Post a Comment

Amiga module player for DOS - the first draft!

After one week, I've finally pulled off what I previously thought impossible - an entire module player, running in real mode DOS! I'...