gbadev.org forum archive

This is a read-only mirror of the content originally found on forum.gbadev.org (now offline), salvaged from Wayback machine copies. A new forum can be found here.

Audio > MOD player fundamentals

#12995 - poslundc - Wed Dec 03, 2003 6:39 am

Well, I've started on the arduous task of coding myself a MOD player. Want to check and make sure I have some of the basic concepts right before I dig my hole too deep.

First of all... my mixed output will be passing through hardware at a frequency of, I guess, 16 KHz seems to be the popular frequency.

If I understand from my research on the MOD format, the sample data for each instrument is stored at in-and-around 8,363 Hz. (Woo, that's an ugly number; close to half of 16 KHz but not quite there.) And different notes are simulated by adjusting this frequency up or down.

So if I understand the concept correctly, my mixer's job will be to keep the sound buffer full by digitally resampling each channel's instrument from its current frequency up to 16 KHz (and then mixing the samples).

The tricky part seems to be that I'm also taking samples from the MOD at an unfixed interval (seems 125 samples/minute is the popular default rate, but this can even change within a MOD). So in effect I'm doing a kind of resampling on that as well.

Is there a general strategy for keeping track of the resamplings for each instrument in each channel, while at the same time keeping track of where you are in the MOD itself? I have a feeling whatever I come up with here is going to be dreadfully inefficient.

Thanks,

Dan.

#12998 - Burton Radons - Wed Dec 03, 2003 7:23 am

Song playing and mixing are two separate tasks; the song player just changes the mixing state (starting and stopping samples and changing their parameters) periodically. You might find it useful to take a generic mixer like Krawall and write a song player out of that.

It seems to be most popular to encode the current sample address is to take the address, subtract 0x08000000 (the ROM segment address), and then convert to fixed-point; 24:8 fixed point allows addressing the full ROM segment. You then calculate a per-sample step size and add that per output sample, like this:

Code:

buffer->point = itofix ((unsigned int) source -  (unsigned int) GBA_ROMSegment);
buffer->step = buffer->frequency * itofix (1) / mixer->frequency;

...
for (each sample in output)
{
    output += *(signed char *) (fixtoi (buffer->point) + GBA_ROMSegment) * bufferVolume >> 8;
    buffer->point += buffer->step;
}


I do song processing every frame, with a bi-level set of counters for the number of frames left in the tick and the number of ticks left in the line. It could be done through timers and an interrupt, but being off by 8 milliseconds is rarely a problem.

#13012 - poslundc - Wed Dec 03, 2003 4:52 pm

Burton Radons wrote:
It seems to be most popular to encode the current sample address is to take the address, subtract 0x08000000 (the ROM segment address), and then convert to fixed-point; 24:8 fixed point allows addressing the full ROM segment. You then calculate a per-sample step size and add that per output sample


Huh, that's a really good idea.

Quote:
I do song processing every frame, with a bi-level set of counters for the number of frames left in the tick and the number of ticks left in the line. It could be done through timers and an interrupt, but being off by 8 milliseconds is rarely a problem.


OK... I think I've got it. Time to forge ahead and see what happens.

Dan.

#13018 - DekuTree64 - Wed Dec 03, 2003 7:10 pm

The frequency you use depends on wether you'd like to mix a new buffer of samples on VBlank, or with a timer interrupt. If you use 16KHz, you'll have to use a timer, but it matches up with the GBA's hardware frequency so the quality might be a tiny bit better. I think GBATEK talks about this in the sound bias reg section.
Then if you want to mix on VBlank, you'll have to use a frequency which divides out evenly with the time spent on each frame. Since frames come at about 59.727Hz, you need a frequency which when divided by that gives you a nice even number of samples to play. Again from GBATEK, in the part about the sound BIOS functions, here's a table of frequencies that work:
5734,7884,10512,13379,15768,18157,21024,26758,31536,36314,40137,42048
To get the buffer size, divide those by 59.727 and round to the nearest int. The most common is 18157, with a buffer size of 304.
Mixing on VBL is good because you know when it will happen, and don't have to worry about blocking other things up. Krawall and AAS both use timers though, so that's fine too.

Then for song timing, I update every VBL and use a fixed point counter to basically resample the song tempo to 60Hz. You might have to play 2 rows in one update if your tempo is too high though. You could use a timer interrupt to do it more exactly, but again this keeps everything nice and synchronized, and I can't tell the difference.

EDIT: And about mixers, I've done a lot of studying on them and written enough mile-long theories about them that I doubt anyone would want to hear about it anymore, so message me on ICQ if you want some info on that.
_________________
___________
The best optimization is to do nothing at all.
Therefore a fully optimized program doesn't exist.
-Deku

#13046 - tepples - Thu Dec 04, 2003 5:54 am

DekuTree64 wrote:
Then for song timing, I update every VBL and use a fixed point counter to basically resample the song tempo to 60Hz. You might have to play 2 rows in one update if your tempo is too high though. You could use a timer interrupt to do it more exactly, but again this keeps everything nice and synchronized, and I can't tell the difference.

Another option that avoids 2-tick updates for tempos 151-255 is to mix at 120 Hz. Run the music engine, mix half a frame, run the music engine, mix half a frame. It should sound marginally better than mixing a whole frame at once.

Quote:
And about mixers, I've done a lot of studying on them and written enough mile-long theories about them that I doubt anyone would want to hear about it anymore, so message me on ICQ if you want some info on that.

Sounds like something you might want to write up on a web page, the way I wrote up sprite cel VRAM swapping.

But there is one slight problem with using 24.8 fixed point for an audio cursor, and that is the fact that the .8 doesn't allow for much precision. In an 18157 Hz mixer, this would imply a playback rate granularity of about 71 Hz. This can throw low-frequency samples quite out of tune. You might try 22.10 instead; this gives only 4 MB of space for samples but fixes the tuning problems. Another option is to store compressed samples in ROM, decompress them to EWRAM, and mix them from there; that lets you use an 18.14 cursor.

EDIT: Corrected off by a factor of 2 error for ROM storage.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.


Last edited by tepples on Thu Dec 04, 2003 5:12 pm; edited 1 time in total

#13048 - poslundc - Thu Dec 04, 2003 6:23 am

tepples wrote:
Another option that avoids 2-tick updates for tempos 151-255 is to mix at 120 Hz. Run the music engine, mix half a frame, run the music engine, mix half a frame. It should sound marginally better than mixing a whole frame at once.


Is there a practical way of doing this if synching to VBlank? Or would I just have to set up a timer?

Quote:
But there is one slight problem with using 24.8 fixed point for an audio cursor, and that is the fact that the .8 doesn't allow for much precision. In an 18157 Hz mixer, this would imply a playback rate granularity of about 71 Hz. This can throw low-frequency samples quite out of tune. You might try 22.10 instead; this gives only 8 MB of space for samples but fixes the tuning problems.


Is there some kind of attribute I can use or other way of specifying to the linker to put all of my sample data at the beginning of the ROM? That would solve that problem nicely.

Dan.

#13053 - tepples - Thu Dec 04, 2003 5:09 pm

poslundc wrote:
tepples wrote:
mix at 120 Hz. Run the music engine, mix half a frame, run the music engine, mix half a frame. It should sound marginally better than mixing a whole frame at once.


Is there a practical way of doing this if synching to VBlank? Or would I just have to set up a timer?

Easy. In your game loop, just run the music code, mix half a frame's worth of audio, run the music code again, and mix the other half. Then trigger the whole thing to play with DMA.

Quote:
Is there some kind of attribute I can use or other way of specifying to the linker to put all of my sample data at the beginning of the ROM?

Usually, the linker puts the .text section at the beginning. If you're using appended datafiles, you can place your sample data last.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#13330 - poslundc - Thu Dec 11, 2003 6:38 am

Well, I'm happy to say I've just about got my MOD player working, except for the effects.

One thing that has stumped me, though, is calculating the intervals at which I need to advance my tick pointer.

The document I have says that the number of ticks per second is equal to the beats-per-minute * 0.4.

So for example, a BPM of 125 would yield 50 ticks per second.

So in my case, where I'm calling my mixer on VBlank (which happens about 60 times/sec), I would want to increment my tick counter by about (50/60) or 0.83 on each VBlank. So I just created a LUT that does that calculation for me for any BPM (in fixed-point format, obviously).

Problem is that 0.83 is way too fast. Way, WAY too fast. I had to hand-tune it down to about 0.5% of that.

Can anyone clarify this formula for me? I'm not sure where I've gone wrong here.

Thanks,

Dan.

#13349 - tepples - Thu Dec 11, 2003 5:24 pm

Here's how I do it: 150 BPM is roughly equivalent to one tick per frame. So maintain a subtick counter whose denominator is 150. On every frame, do something to this effect:
Code:
subtick += tempo;
while(subtick >= 150)
{
  process_one_tick();
  subtick -= 150;
}

_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.