Jump to content
  • Advertisement

Archived

This topic is now archived and is closed to further replies.

Crispy

Writing a .mod player

This topic is 5787 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I was wondering if anyone here has successfully written a mod player for the PC (I''m using DSound for playback with software mixing and resampling). I have a load of questions and stuff I can''t get right (as all of the documentation I can find is for the Amiga and GUS), but I''d like to know first if it''s worth all the explaining if no one can actually understand what I''m talking about...

Share this post


Link to post
Share on other sites
Advertisement
Have you checked fmoddoc2.zip? Just put the filename in google and you''ll find it. I don''t remember if it had non-GUS stuff, but I remember it is very detailed.

Share this post


Link to post
Share on other sites
fmoddoc2.zip is an incredible resource for mod programming. Personally, I''ve written a mod, s3m and xm player. What kind of questions did you have?

Share this post


Link to post
Share on other sites
Thanks for the replies. I had a look at fmoddoc2.zip. It deals with GUS - something I don''t want to hear about.

I am writing my mod player from scratch - so far I haven''t had one single look at others'' source code. I''m trying my best to do it according to the documentation I can find. I haven''t gone through the documentation contained within fmoddoc2.zip yet, but since the example provided with it runs on GUS, I''m assuming the documentation also only covers that aspect.

The primary problem (among others) that I''m currently stuck with, is playback. All the buffering and stuff is easy - the problem lies in the pitch shifting and calculating proper playback sampling rates for instruments. Stemming from that:

1) There is no standard sampling rate for instruments in .mod files. So far I have come across 11025 Hz 16 bit data only, except for drums which seem to be sampled at arbitrary rates (just as is said in the docs). However, there is no indication in the file or in the docs as to how I am supposed to know the native sampling rate of an instrument. I''m using MadTracker (as it''s free and runs on my Win2k) as a role model. I came up with the following formula:

SamplingRate = 7093789.2 / (SamplePeriod * 4.f);

that makes my player sound roughly like MadTracker, except that the percussions are off. Obviously this is due to the fact that they''re sampled at a different rate. How do I fix this and what should the real formula look like (if there is such a thing; this one''s only a modification of the PAL formula provided for the Amiga)?

2) How is the pitch shifting done? I''m currently using my own implementation of bandlimited interpolation that is kind of lousy - the sound is hissy (even though the pitch shift is OK) and the overall quality is not so good. For example, what method does MadTracker use (if anyone happen to know; it''s sound is so much cleaner and better than my player''s). I don''t want to do hardware shifting and mixing - it''s fake and doesn''t make the code that easily portable.

That should be enough for now - there''s a couple of questions related to the loading code (only about 50% of the mod''s are loaded properly even though I think I''m handling most known versions of the format...), but I haven''t had that good a look at it yet, and a few of the effects, but I haven''t had too thorough a look at those either...

Share this post


Link to post
Share on other sites
Actually, fmoddoc2.zip has a file specifically devoted to sound blaster mixing (ie. straight stream mixing, it also has some historical info on how to program the registers etc.). It also has great historical context which is quite useful to get your mind back into the 80''s .

1. There is no sampling information . You merely grab x # of bytes at the rate specified by the note (of course a tad simplified). Check section 3.5 in fmoddoc.txt for a detailed discussion of this.

2. The thing that is valuable to keep in mind when working on a mod player is that it was originally designed to be as blazingly fast as possible. Pitch shifting simply involves stepping through the sample data faster or slower. Most early mod players didn''t even use linear interpolation between sample values.

Also, your hissy sound might be coming from other sources, such as your mixing routines which might be overflowing, or otherwise incorrectly clamped.

Share this post


Link to post
Share on other sites
Okay, I went thourgh the .mod documentation in fmoddoc2. It suggests using interrupt vectors for playback on the PC which AFAIK directly relates to DOS. I''m pretty sure you can''t send interrupts directly to any of the devices in the computer in Windows - the OS will simply block them (that''s why old DOS games won''t run on newer Windowses). This still leaves me somewhat unsure as to how Win32 trackers do it. I guess, one way would be to set up a DSound buffer and modify the playback frequency at every tick, but that would mean using up up to 64 buffers just for music playback and that doesn''t look too appealing...

Premandrake: did you write your player for Windows and exactly what method did you use? If you didn''t write the plyback routine yourself then where did you look it up?

Regarding the drum sounds - hmm, they still sound too low. This could be the finetune playing tricks, though, because I haven''t implemented support for it yet.

Share this post


Link to post
Share on other sites

You can''t do MODs with DirectSound buffers, unless you mix them yourself. Okay, I take that little back, you can do *basic* stuff, but looping and such stuff are PITA to write with DSound. Write your own mixer.

Of fmoddoc, skip everything that is directly related to hardware. Sample mixing speeds however ARE NOT hardware-related.

Custom mixer that uses DSound or WaveOut is THE way to go. It''s really quite simple; if you don''t care about performance you can simply use floats.

Then, that formula you presented for calculation of frequencies is way off. When playing MODs (protracker modules; s3m has a similar and xm quite different scheme for frequency calculation), sample headers have value usually called C3Freq or C4Freq; this is the samplerate of this sample when playing note C4 (C of octave 3). Consult the docs how this value is used to calculate actual output frequency of sample.

Share this post


Link to post
Share on other sites
quote:
Original post by Hway
You can''t do MODs with DirectSound buffers, unless you mix them yourself. Okay, I take that little back, you can do *basic* stuff, but looping and such stuff are PITA to write with DSound. Write your own mixer.



But I am mixing everything in software (as I already stated above). The problem is in pitch shifting - there only seem to be 3 eligible solutions to obtain normal sound quality:

1) bandlimited interpolation (the one I''m using) which is relatively fast, but must be done in software. Downsides: I have no idea if I got the code right. Probably not because the sound is not clean.
2) Fourier Transform: messes up all mod''s because it doesn''t follow the normal rules of increased data throughput at higher frequencies which makes the songs sound more natural, is computationally slower and gives a new feel to it all. Besides, this isn''t how mod''s are played back by definition.
3) Hardware mixing: not an option at this point

quote:

Of fmoddoc, skip everything that is directly related to hardware. Sample mixing speeds however ARE NOT hardware-related.



Proper mixing has to get pass such terms as multiplication and decimation which cancels out linear sample appriximation (linear interpolation). Hence the filterized bandlimited interpolation method. Is there some other, perhaps faster method that I''m not aware of?

quote:

Custom mixer that uses DSound or WaveOut is THE way to go. It''s really quite simple; if you don''t care about performance you can simply use floats.



Are you suggesting linear interoplation through floats? Hmm - I don''t really see how this could work...

quote:

Then, that formula you presented for calculation of frequencies is way off. When playing MODs (protracker modules; s3m has a similar and xm quite different scheme for frequency calculation), sample headers have value usually called C3Freq or C4Freq; this is the samplerate of this sample when playing note C4 (C of octave 3). Consult the docs how this value is used to calculate actual output frequency of sample.



As I said earlier, this is a makeshift formula that is tuned to sound like MadTracker. I have no clue as to what the correct formula is or should be...

Share this post


Link to post
Share on other sites
Ok, for playback in windows, you have various options. The one I chose (for compatibility, directx has a lower latency but doesn''t like NT4) was to use WaveOut. I will post ad-hoc the wave out code I used here.

  
#include "stdafx.h"
#include "playback.h"

typedef DWORD dword;
typedef unsigned short word;
typedef unsigned char byte;
typedef signed char s8;
typedef signed short s16;
typedef signed int s32;
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;

static WAVEFORMATEX pcmwf;
static HWAVEOUT g_waveOutHandle;
static WAVEHDR wavehdr;
static int g_blockSize;
static int g_latency = 20;
static int g_bufferSize;
static int g_fillBlock;
static int g_realBlock;
static s8 *g_data;
static s8 *g_mixBufMem;
static s8 *g_mixBuf;
static bool g_threadAlive = false;
extern HWND g_hWnd;

bool isPlaying = false;

void InitSound()
{
HRESULT hr;

// Set up wave format structure.

memset(&pcmwf, 0, sizeof(PCMWAVEFORMAT));
pcmwf.wFormatTag = WAVE_FORMAT_PCM;
pcmwf.wBitsPerSample = 16;
pcmwf.nChannels = 2;
pcmwf.nBlockAlign = pcmwf.nChannels * pcmwf.wBitsPerSample / 8;
pcmwf.nSamplesPerSec = MIXRATE;
pcmwf.nAvgBytesPerSec = pcmwf.nSamplesPerSec * pcmwf.nBlockAlign;
pcmwf.cbSize = 0;

// Open the waveout

hr = waveOutOpen(&g_waveOutHandle,WAVE_MAPPER,&pcmwf,0,0,0);
if (hr)
{
MessageBox(g_hWnd,"Error","Unable to initialize sound",MB_OK);
return;
}

g_blockSize = ((MIXRATE * g_latency / 1000) + 3) & 0xFFFFFFFC;
// Keep a double buffer

g_bufferSize = (g_blockSize * (1000 / g_latency))*2;
}

void ShutdownSound()
{
waveOutReset(g_waveOutHandle);
waveOutClose(g_waveOutHandle);
}

void StopSound()
{
waveOutUnprepareHeader(g_waveOutHandle,&wavehdr,sizeof(WAVEHDR));
wavehdr.dwFlags &= ~WHDR_PREPARED;

waveOutReset(g_waveOutHandle);
}

void MixerClipCopy(void *src, void *dest,int len)
{
s16 *destptr = (s16*)dest;
float *srcptr = (float*)src;

if (len <=0 || !dest || !src) return;

for (int count=0; count<len<<1; count++)
{
int val;
__asm
{
mov eax, srcptr
fld [eax]
add srcptr, 4
fistp val
}
*destptr++ = (val < -32768 ? -32768 : val > 32767 ? 32767 : val);
}
}

void FillBuffer()
{
void *mixBuf;
int mixPos = g_fillBlock * g_blockSize;
int totalBlocks = g_bufferSize / g_blockSize;

mixBuf = (s8*)g_mixBuf + (mixPos << 3);

// Clear mix buffer

memset(mixBuf,0,g_blockSize<<3);

// Mix the sounds

if (engine.Mix((float*)mixBuf,g_blockSize)==-1)
{
isPlaying = false;
}

// Clip and copy to output buffer

MixerClipCopy(mixBuf,g_data+(mixPos<<2),g_blockSize);

// Go to next

g_fillBlock++;
g_realBlock++;
if (g_fillBlock >= totalBlocks) g_fillBlock = 0;
}

DWORD CALLBACK UpdateThread(void *data)
{
MMTIME mmt;
int length;
int cursorPos,cursorBlock;
int cursorReal = 0;

g_threadAlive = true;

length = g_bufferSize;
length <<= 2; // 16 bit, stereo


g_data = (s8*)malloc(length);
memset(g_data,0,length);

wavehdr.dwFlags = WHDR_BEGINLOOP | WHDR_ENDLOOP;
wavehdr.lpData = (LPSTR)g_data;
wavehdr.dwBufferLength = length;
wavehdr.dwBytesRecorded = 0;
wavehdr.dwUser = 0;
wavehdr.dwLoops = -1;
waveOutPrepareHeader(g_waveOutHandle,&wavehdr,sizeof(WAVEHDR));

g_mixBufMem = (s8*)malloc((g_bufferSize << 3) + 256);
memset(g_mixBufMem,0,(g_bufferSize << 3) + 256);
g_mixBuf = (s8*)(((unsigned int)g_mixBufMem + 15) & 0xFFFFFFF0);

// Prefill the buffer

g_fillBlock = 0;
g_realBlock = 0;
isPlaying = true;

do
{
FillBuffer();
} while (g_fillBlock != 0 && isPlaying);

// Start the output

waveOutWrite(g_waveOutHandle,&wavehdr,sizeof(WAVEHDR));

// Do the mixing

while (isPlaying)
{
mmt.wType = TIME_BYTES;
waveOutGetPosition(g_waveOutHandle,&mmt,sizeof(MMTIME));
mmt.u.cb >>= 2;
cursorPos = mmt.u.cb;

cursorPos %= g_bufferSize;
cursorBlock = cursorPos / g_blockSize;

while (cursorBlock != g_fillBlock && isPlaying)
{
FillBuffer();
}

Sleep(5);
}

while (cursorReal < g_realBlock-2)
{
Sleep(5);
mmt.wType = TIME_BYTES;
waveOutGetPosition(g_waveOutHandle,&mmt,sizeof(MMTIME));
mmt.u.cb >>= 2;
cursorPos = mmt.u.cb;
cursorReal = cursorPos / g_blockSize;
}

StopSound();

free(g_mixBufMem);
free(g_data);

g_threadAlive = false;

return 0;
}

void PlayMixedSound()
{
if (g_threadAlive) return;

engine.ResetSound();
CreateThread(NULL,0,UpdateThread,0,0,0);
}

You call it like this:

  
// In your init

InitSound();

// To play a stream

PlayMixedSound();

// In your shutdown routine

ShutdownSound();

The important part here is:

// Mix the sounds
if (engine.Mix((float*)mixBuf,g_blockSize)==-1)
{
isPlaying = false;
}

Which calls my mixing code to fill a buffer with g_blockSize floats. It then converts the floats to 16 bit signed values for 16-bit stereo playback using MixerClipCopy.

Basically this code creates a looping buffer that is constantly being played and filled at the same time. The two cursors are just in different locations.

Now onto the actual mixing portion of the code, I''m not going to copy and paste that in because it''s disgusting . However, the basic idea is:

  
void Engine::Step() {
if (tick == 0) { } // Update notes

else { } // Update per tick effects


tick++;
if (tick == tempo) tick = 0;

m_numToMix = ((MIX_RATE * 10)/bpm)>>2;
}

int Engine::Mix(float *buffer,int length) {
if (m_numToMix <= 0) Step();

for (;;) {
numToMix = min(length,m_numToMix);
for (each channel) {
// mix numToMix floats doing looping, interpolation, etc..

for (i=0; i<numToMix; i++) buffer[i] += channel value;
}
m_numToMix -= numToMix;
buffer += numToMix;
if (m_numToMix <= 0) Step();
}

for (i=0; i<length; i++) buffer[i] /= activeChannels;
}

So the basic idea here is that we mix the number of floats in one tick, then step the player again to get the value for the next tick.

I also know how reading through fmoddoc2 must seem, but it really does have a lot of great information once you strip out the stuff that is irrelevant to the modern PC. For example, the never ending buffer trick is just adapted from the IRQ method, instead of having the soundcard let us know when it needs more, it''s Windows that does it .

Hope this is of some help, feel free to ask questions about the code and I''ll try to answer them.

Share this post


Link to post
Share on other sites

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!