/* TinyMOD Written by Tammo "kb" Hinrichs in 2007 This source code is hereby placed into the public domain. Use, distribute, modify, misappropriate and generally abuse it as you wish. Giving credits would be nice of course. This player includes an Amiga Paula chip "emulation" that faithfully recreates how it sounds when a sample is resampled using a master clock of 3.5 MHz. Yes, rendering at this rate and downsampling to the usual 48KHz takes quite a bit of CPU. Feel free to replace this part with some conventional mixing routines if authenticity isn't your goal and you really need Protracker MOD support for any other reason... The code should be pretty portable, all OS/platform dependent stuff is at the top. Code for testing is at the bottom. You'll need some kind of sound output that calls back the player providing it a stereo interleaved single float buffer to write into (0dB=1.0). Changelog: 2007-12-07: * fixed 40x and 4x0 vibrato effects (jogeir - tiny tunes) * fixed pattern loop (olof gustafsson - pinball illusions) * fixed fine volslide down (olof gustafsson - pinball illusions) * included some external header files * cleanups 2007-12-06: first "release". Note to self: Don't post stuff on pouet.net when drunk. */ //------------------------------------------------------------------------------ // system dependent stuff starts here #define _CRT_SECURE_NO_DEPRECATE #include #include #pragma intrinsic (memset, sqrt, sin, cos, atan, pow) typedef int sInt; typedef unsigned int sUInt; typedef sInt sBool; typedef char sChar; typedef signed char sS8; typedef signed short sS16; typedef signed long sS32; typedef signed __int64 sS64; typedef unsigned char sU8; typedef unsigned short sU16; typedef unsigned long sU32; typedef unsigned __int64 sU64; typedef float sF32; typedef double sF64; inline void sZeroMem(void *dest, sInt size) { memset(dest,0,size); } inline sF32 sFSqrt(sF32 x) { return sqrtf(x); } inline sF32 sFSin(sF32 x) { return sinf(x); } inline sF32 sFCos(sF32 x) { return cosf(x); } inline sF32 sFAtan(sF32 x) { return atanf(x); } inline sF32 sFPow(sF32 b, sF32 e) { return powf(b,e); } inline void sSwapEndian(sU16 &v) { v=((v&0xff)<<8)|(v>>8); } // system dependent stuff ends here //------------------------------------------------------------------------------ const sF32 sFPi=4*sFAtan(1); template inline T sMin(const T a, const T b) { return (a inline T sMax(const T a, const T b) { return (a>b)?a:b; } template inline T sClamp(const T x, const T min, const T max) { return sMax(min,sMin(max,x)); } template T sSqr(T v) { return v*v; } template T sLerp(T a, T b, sF32 f) { return a+f*(b-a); } template T sAbs(T x) { return abs(x); } inline sF32 sFSinc(sF32 x) { return x?sFSin(x)/x:1; } inline sF32 sFHamming(sF32 x) { return (x>-1 && x<1)?sSqr(sFCos(x*sFPi/2)):0;} union sIntFlt { sU32 U32; sF32 F32; }; const sInt PAULARATE=3740000; // approx. pal timing const sInt OUTRATE=48000; // approx. pal timing const sInt OUTFPS=50; // approx. pal timing //------------------------------------------------------------------------------ class Paula { public: static const sInt FIR_WIDTH=512; sF32 FIRMem[2*FIR_WIDTH+1]; struct Voice { private: sInt Pos; sInt PWMCnt, DivCnt; sIntFlt Cur; public: sS8* Sample; sInt SampleLen; sInt LoopLen; sInt Period; // 124 .. 65535 sInt Volume; // 0 .. 64 Voice() : Period(65535), Volume(0), Sample(0), Pos(0), PWMCnt(0), DivCnt(0), LoopLen(1) { Cur.F32=0; } void Render(sF32 *buffer, sInt samples) { if (!Sample) return; sU8 *smp=(sU8*)Sample; for (sInt i=0; iWritePos) Calc(); sF32 outl0=0, outl1=0; sF32 outr0=0, outr1=0; // this needs optimization. SSE would come to mind. sInt offs=(ReadPos-FIR_WIDTH-1)&(RBSIZE-1); sF32 vl=RingBuf[offs]; sF32 vr=RingBuf[offs+RBSIZE]; for (sInt i=1; i<2*FIR_WIDTH-1; i++) { sF32 w=FIRMem[i]; outl0+=vl*w; outr0+=vr*w; offs=(offs+1)&(RBSIZE-1); vl=RingBuf[offs]; vr=RingBuf[offs+RBSIZE]; outl1+=vl*w; outr1+=vr*w; } sF32 outl=sLerp(outl0,outl1,ReadFrac); sF32 outr=sLerp(outr0,outr1,ReadFrac); *outbuf++=vm0*outl+vm1*outr; *outbuf++=vm1*outl+vm0*outr; ReadFrac+=step; sInt rfi=sInt(ReadFrac); ReadPos=(ReadPos+rfi)&(RBSIZE-1); ReadFrac-=rfi; } } Paula() { // make FIR table sF32 *FIRTable=FIRMem+FIR_WIDTH; sF32 yscale=sF32(OUTRATE)/sF32(PAULARATE); sF32 xscale=sFPi*yscale; for (sInt i=-FIR_WIDTH; i<=FIR_WIDTH; i++) FIRTable[i]=yscale*sFSinc(sF32(i)*xscale)*sFHamming(sF32(i)/sF32(FIR_WIDTH-1)); sZeroMem(RingBuf,sizeof(RingBuf)); ReadPos=0; ReadFrac=0; WritePos=FIR_WIDTH; MasterVolume=0.66f; MasterSeparation=0.5f; //FltBuf=0; } }; //------------------------------------------------------------------------------ class ModPlayer { Paula *P; static sInt BasePTable[5*12+1]; static sInt PTable[16][60]; static sInt VibTable[3][15][64]; struct Sample { char Name[22]; sU16 Length; sS8 Finetune; sU8 Volume; sU16 LoopStart; sU16 LoopLen; void Prepare() { sSwapEndian(Length); sSwapEndian(LoopStart); sSwapEndian(LoopLen); Finetune&=0x0f; if (Finetune>=8) Finetune-=16; } }; struct Pattern { struct Event { sInt Sample; sInt Note; sInt FX; sInt FXParm; } Events[64][4]; Pattern() { sZeroMem(this,sizeof(Pattern)); } void Load(sU8 *ptr) { for (sInt row=0; row<64; row++) for (sInt ch=0; ch<4; ch++) { Event &e=Events[row][ch]; e.Sample = (ptr[0]&0xf0)|(ptr[2]>>4); e.FX = ptr[2]&0x0f; e.FXParm = ptr[3]; e.Note=0; sInt period = (sInt(ptr[0]&0x0f)<<8)|ptr[1]; sInt bestd = sAbs(period-BasePTable[0]); if (period) for (sInt i=1; i<=60; i++) { sInt d=sAbs(period-BasePTable[i]); if (d7) { offs++; ft-=16; } while (ft<-8) { offs--; ft+=16; } return Note?(PTable[ft&0x0f][sClamp(Note+offs-1,0,59)]):0; } void SetPeriod(sInt offs=0, sInt fineoffs=0) { if (Note) Period=GetPeriod(offs,fineoffs); } } Chans[4]; sInt Speed; sInt TickRate; sInt TRCounter; sInt CurTick; sInt CurRow; sInt CurPos; sInt Delay; void CalcTickRate(sInt bpm) { TickRate=(125*OUTRATE)/(bpm*OUTFPS); } void TrigNote(sInt ch, const Pattern::Event &e) { Chan &c=Chans[ch]; Paula::Voice &v=P->V[ch]; const Sample &s=Samples[c.Sample]; sInt offset=0; if (e.FX==9) offset=c.FXBuf[9]<<8; if (e.FX!=3 && e.FX!=5) { c.SetPeriod(); if (s.LoopLen>1) v.Trigger(SData[c.Sample],2*(s.LoopStart+s.LoopLen),2*s.LoopLen,offset); else v.Trigger(SData[c.Sample],v.SampleLen=2*s.Length,1,offset); if (!c.VibRetr) c.VibPos=0; if (!c.TremRetr) c.TremPos=0; } } void Reset() { CalcTickRate(125); Speed=6; TRCounter=0; CurTick=0; CurRow=0; CurPos=0; Delay=0; } void Tick() { const Pattern &p=Patterns[PatternList[CurPos]]; const Pattern::Event *re=p.Events[CurRow]; for (sInt ch=0; ch<4; ch++) { const Pattern::Event &e=re[ch]; Paula::Voice &v=P->V[ch]; Chan &c=Chans[ch]; const sInt fxpl=e.FXParm&0x0f; sInt TremVol=0; if (!CurTick) { if (e.Sample) { c.Sample=e.Sample; c.FineTune=Samples[c.Sample].Finetune; c.Volume=Samples[c.Sample].Volume; } if (e.FXParm) c.FXBuf[e.FX]=e.FXParm; if (e.Note && (e.FX!=14 || ((e.FXParm>>4)!=13))) { c.Note=e.Note; TrigNote(ch,e); } switch (e.FX) { case 4: // vibrato case 6: if (c.FXBuf[4]&0x0f) c.VibAmpl=c.FXBuf[4]&0x0f; if (c.FXBuf[4]&0xf0) c.VibSpeed=c.FXBuf[4]>>4; c.SetPeriod(0,VibTable[c.VibWave][(c.VibAmpl)-1][c.VibPos]); break; case 7: // tremolo if (c.FXBuf[7]&0x0f) c.TremAmpl=c.FXBuf[7]&0x0f; if (c.FXBuf[7]&0xf0) c.TremSpeed=c.FXBuf[7]>>4; TremVol=VibTable[c.TremWave][(c.TremAmpl)-1][c.TremPos]; break; case 12: // set vol c.Volume=sClamp(e.FXParm,0,64); break; case 14: // special if (fxpl) c.FXBuf14[e.FXParm>>4]=fxpl; switch (e.FXParm>>4) { case 0: // set filter break; case 1: // fineslide up c.Period=sMax(113,c.Period-c.FXBuf14[1]); break; case 2: // slide down c.Period=sMin(856,c.Period+c.FXBuf14[2]); break; case 3: // set glissando sucks! break; case 4: // set vib waveform c.VibWave=fxpl&3; if (c.VibWave==3) c.VibWave=0; c.VibRetr=fxpl&4; break; case 5: // set finetune c.FineTune=fxpl; if (c.FineTune>=8) c.FineTune-=16; break; case 7: // set tremolo c.TremWave=fxpl&3; if (c.TremWave==3) c.TremWave=0; c.TremRetr=fxpl&4; break; case 9: // retrigger if (c.FXBuf14[9] && !e.Note) TrigNote(ch,e); c.RetrigCount=0; break; case 10: // fine volslide up c.Volume=sMin(c.Volume+c.FXBuf14[10],64); break; case 11: // fine volslide down; c.Volume=sMax(c.Volume-c.FXBuf14[11],0); break; case 14: // delay pattern Delay=c.FXBuf14[14]; break; case 15: // invert loop (WTF) break; } break; case 15: // set speed if (e.FXParm) if (e.FXParm<=32) Speed=e.FXParm; else CalcTickRate(e.FXParm); break; } } else { switch (e.FX) { case 0: // arpeggio if (e.FXParm) { sInt no=0; switch (CurTick%3) { case 1: no=e.FXParm>>4; break; case 2: no=e.FXParm&0x0f; break; } c.SetPeriod(no); } break; case 1: // slide up c.Period=sMax(113,c.Period-c.FXBuf[1]); break; case 2: // slide down c.Period=sMin(856,c.Period+c.FXBuf[2]); break; case 5: // slide plus volslide if (c.FXBuf[5]&0xf0) c.Volume=sMin(c.Volume+(c.FXBuf[5]>>4),0x40); else c.Volume=sMax(c.Volume-(c.FXBuf[5]&0x0f),0); // no break! case 3: // slide to note { sInt np=c.GetPeriod(); if (c.Period>np) c.Period=sMax(c.Period-c.FXBuf[3],np); else if (c.Period>4),0x40); else c.Volume=sMax(c.Volume-(c.FXBuf[6]&0x0f),0); // no break! case 4: // vibrato ??? c.SetPeriod(0,VibTable[c.VibWave][c.VibAmpl-1][c.VibPos]); c.VibPos=(c.VibPos+c.VibSpeed)&0x3f; break; case 7: // tremolo ??? TremVol=VibTable[c.TremWave][c.TremAmpl-1][c.TremPos]; c.TremPos=(c.TremPos+c.TremSpeed)&0x3f; break; case 10: // volslide if (c.FXBuf[10]&0xf0) c.Volume=sMin(c.Volume+(c.FXBuf[10]>>4),0x40); else c.Volume=sMax(c.Volume-(c.FXBuf[10]&0x0f),0); break; case 11: // pos jump if (CurTick==Speed-1) { CurRow=-1; CurPos=e.FXParm; } break; case 13: // pattern break if (CurTick==Speed-1) { CurPos++; CurRow=(10*(e.FXParm>>4)+(e.FXParm&0x0f))-1; } break; case 14: // special switch (e.FXParm>>4) { case 6: // loop pattern if (!fxpl) // loop start c.LoopStart=CurRow; else if (CurTick==Speed-1) { if (c.LoopCount=Speed*(Delay+1)) { CurTick=0; CurRow++; Delay=0; } if (CurRow>=64) { CurRow=0; CurPos++; } if (CurPos>=PositionCount) CurPos=0; }; public: char Name[21]; ModPlayer(Paula *p, sU8 *moddata) : P(p) { // calc ptable for (sInt ft=0; ft<16; ft++) { sInt rft= -((ft>=8)?ft-16:ft); sF32 fac=sFPow(2.0f,sF32(rft)/(12.0f*16.0f)); for (sInt i=0; i<60; i++) PTable[ft][i]=sInt(sF32(BasePTable[i])*fac+0.5f); } // calc vibtable for (sInt ampl=0; ampl<15; ampl++) { sF32 scale=ampl+1.5f; sF32 shift=0; for (sInt x=0; x<64; x++) { VibTable[0][ampl][x]=sInt(scale*sFSin(x*sFPi/32.0f)+shift); VibTable[1][ampl][x]=sInt(scale*((63-x)/31.5f-1.0f)+shift); VibTable[2][ampl][x]=sInt(scale*((x<32)?1:-1)+shift); } } // "load" the mod memcpy(Name,moddata,20); Name[20]=0; moddata+=20; SampleCount=16; ChannelCount=4; Samples=(Sample*)(moddata-sizeof(Sample)); moddata+=15*sizeof(Sample); sU32 &tag=*(sU32*)(moddata+130+16*sizeof(Sample)); switch (tag) { case '.K.M': case '4TLF': case '!K!M': SampleCount=32; break; } if (SampleCount>16) moddata+=(SampleCount-16)*sizeof(Sample); for (sInt i=1; i15) moddata+=4; // skip tag PatternCount=0; for (sInt i=0; i<128; i++) PatternCount=sClamp(PatternCount,PatternList[i]+1,128); for (sInt i=0; i(len,TRCounter); if (todo) { P->Render(buf,todo); buf+=2*todo; len-=todo; TRCounter-=todo; } else { Tick(); TRCounter=TickRate; } } return 1; } static sU32 __stdcall RenderProxy(void *parm, sF32 *buf, sU32 len) { return ((ModPlayer*)parm)->Render(buf,len); } }; sInt ModPlayer::BasePTable[61]= { 0, 1712,1616,1525,1440,1357,1281,1209,1141,1077,1017, 961, 907, 856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453, 428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226, 214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113, 107, 101, 95, 90, 85, 80, 76, 71, 67, 64, 60, 57, }; sInt ModPlayer::PTable[16][60]; sInt ModPlayer::VibTable[3][15][64]; //------------------------------------------------------------------------------ // ok, let's test it: #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers #include #include "dsio.h" // good luck. static sU8 mod[4*1024*1024]; int __cdecl main(int argc, const char **argv) { if (argc<2) { MessageBox(0,"Usage: tinymod ","TinyMOD",MB_OK); return 1; } HANDLE fh=CreateFile(argv[1],GENERIC_READ,FILE_SHARE_READ,0,OPEN_EXISTING,0,0); if (fh==INVALID_HANDLE_VALUE) { MessageBox(0,"couldn't open file!","TinyMOD",MB_OK); return 1; } sInt size=GetFileSize(fh,0); DWORD read; ReadFile(fh,mod,size,&read,0); CloseHandle(fh); Paula P; ModPlayer player(&P,mod); dsInit(player.RenderProxy,&player,GetForegroundWindow()); MessageBox(0,player.Name,"TinyMOD",MB_OK); dsClose(); return 0; }