Compare commits

9 Commits
Author SHA1 Message Date
Jason 3e5ea9e7c5 Create blog post on Amiga sound chip and MOD player
Added a blog post about the Amiga's sound chip and a .MOD player project.
2026-05-05 22:40:35 +10:00
Jason 82a41e1b30 Add files via upload 2026-05-05 22:31:46 +10:00
Jason 17164b02d5 Rename types.h to r1.0/types.h 2026-05-05 22:26:02 +10:00
Jason 4c6ba830e6 Rename tinymod.cpp to r1.0/tinymod.cpp 2026-05-05 22:25:32 +10:00
Jason 5df9049ee6 Rename paula.h to r1.0/paula.h 2026-05-05 22:24:54 +10:00
Jason e60184d043 Rename modplayer.h to r1.0/modplayer.h 2026-05-05 22:24:03 +10:00
Jason 5099e60803 Create .gitkeep 2026-05-05 22:20:55 +10:00
Jason dad35bf491 Create .gitkeep 2026-05-05 22:19:53 +10:00
Jason 167f0b7c46 Merge pull request #1 from bou-samra/refactored
Refactor: modularize code into separate files with enhanced documenta…
2026-04-24 19:40:13 +10:00
9 changed files with 1511 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
+15
View File
@@ -0,0 +1,15 @@
# kb's blog
random and not so random stuff/
**TinyMOD**
*Posted on 2011/10/23*
At some point in 2007 somebody told me how the Amigas sound chip, Paula, was able to modify a sounds volume digitally without using multiplication dedicated circuits for that would have been prohibitively expensive for a home computer in 1984: It simply has a 6-bit counter per voice thats incremented every cycle and if its value is above the set volume, the voice is silenced for that cycle. So effectively its PWM with a pulse frequency of about 50Khz.
“But wait, shouldnt that color the sound, ring modulation artifacts and such?” I thought. The answer is of course a resounding no (also all artifacts introduced by the PWM are outside the audible range) but that didnt stop me from trying to emulate a Paula voice at the full 3.5MHz and then filtering it down to find out how it sounds.
Yeah well, and while were at it, lets see if we can hack up a simple .MOD player too without using anything a 68000 didnt have to offer (multiplications and such). Because coding for a couple of hours and the only being able to play a single waveform is boring.
Another few hours later there was one additional never-to-be-published toy project on my HD that way able to play a few MOD files that I liked, and that was about to be abandoned… if it hadnt been for a thread on pouet.net where somebody was asking for a module player source. And I just came home from a party and was ever so slightly inebriated, so I just pasted the source code there. A discussion spawned, I cleaned up the code a bit and fixed some replay errors, and so here it is, released into the public domain for everyone to enjoy or laugh at:
Just add sound output. Have fun :)
+706
View File
@@ -0,0 +1,706 @@
// test.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "dsio.h"
const sInt PAULARATE=3740000; // approx. pal timing
const sInt OUTRATE=48000; // approx. pal timing
const sInt OUTFPS=50; // approx. pal timing
//------------------------------------------------------------------------------
const sF32 sFPi=4*atanf(1);
union sIntFlt { sU32 U32; sF32 F32; };
template<typename T> T sSqr(T v) { return v*v; }
template<typename T> T sLerp(T a, T b, sF32 f) { return a+f*(b-a); }
template<typename T> T sAbs(T x) { return abs(x); }
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 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;}
inline sF32 sFPow(sF32 b, sF32 e) { return powf(b,e); }
inline void sSetMem(void *dest, sU8 v, sInt size) { memset(dest,v,size); }
inline void sCopyMem(void *dest, const void *src, sInt size) { memcpy(dest,src,size); }
inline void sZeroMem(void *dest, sInt size) { sSetMem(dest,0,size); }
//------------------------------------------------------------------------------
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 || !Volume) return;
sU8 *smp=(sU8*)Sample;
for (sInt i=0; i<samples; i++)
{
if (!DivCnt)
{
// todo: use a fake d/a table for this
Cur.U32=((smp[Pos]^0x80)<<15)|0x40000000;
Cur.F32-=3.0f;
if (++Pos==SampleLen) Pos-=LoopLen;
DivCnt=Period;
}
if (PWMCnt<Volume) buffer[i]+=Cur.F32;
PWMCnt=(PWMCnt+1)&0x3f;
DivCnt--;
}
}
void Trigger(sS8 *smp,sInt sl, sInt ll, sInt offs=0)
{
Sample=smp;
SampleLen=sl;
LoopLen=ll;
Pos=sMin(offs,SampleLen-1);
}
};
Voice V[4];
// rendering in paula freq
static const sInt RBSIZE = 4096;
sF32 RingBuf[2*RBSIZE];
sInt WritePos;
sInt ReadPos;
sF32 ReadFrac;
//sF32 FltFreq;
//sF32 FltBuf;
void CalcFrag(sF32 *out, sInt samples)
{
sZeroMem(out,sizeof(sF32)*samples);
sZeroMem(out+RBSIZE,sizeof(sF32)*samples);
for (sInt i=0; i<4; i++)
{
if (i==1 || i==2)
V[i].Render(out+RBSIZE,samples);
else
V[i].Render(out,samples);
}
}
void Calc() // todo: stereo
{
sInt RealReadPos=ReadPos-FIR_WIDTH-1;
sInt samples=(RealReadPos-WritePos)&(RBSIZE-1);
sInt todo=sMin(samples,RBSIZE-WritePos);
CalcFrag(RingBuf+WritePos,todo);
if (todo<samples)
{
WritePos=0;
todo=samples-todo;
CalcFrag(RingBuf,todo);
}
WritePos+=todo;
};
sF32 MasterVolume;
sF32 MasterSeparation;
// rendering in output freq
void Render(sF32 *outbuf, sInt samples)
{
const sF32 step=sF32(PAULARATE)/sF32(OUTRATE);
const sF32 pan=0.5f+0.5f*MasterSeparation;
const sF32 vm0=MasterVolume*sFSqrt(pan);
const sF32 vm1=MasterVolume*sFSqrt(1-pan);
for (sInt s=0; s<samples; s++)
{
sInt ReadEnd=ReadPos+FIR_WIDTH+1;
if (WritePos<ReadPos) ReadEnd-=RBSIZE;
if (ReadEnd>WritePos) 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 inline void SwapEndian(sU16 &v) { v=((v&0xff)<<8)|(v>>8); }
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()
{
SwapEndian(Length);
SwapEndian(LoopStart);
SwapEndian(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 (d<bestd)
{
bestd=d;
e.Note=i;
}
}
ptr+=4;
}
}
};
Sample *Samples;
sS8 *SData[32];
sInt SampleCount;
sInt ChannelCount;
sU8 PatternList[128];
sInt PositionCount;
sInt PatternCount;
Pattern Patterns[128];
struct Chan
{
sInt Note;
sInt Period;
sInt Sample;
sInt FineTune;
sInt Volume;
sInt FXBuf[16];
sInt FXBuf14[16];
sInt LoopStart;
sInt LoopCount;
sInt RetrigCount;
sInt VibWave;
sInt VibRetr;
sInt VibPos;
sInt TremWave;
sInt TremRetr;
sInt TremPos;
Chan() { sZeroMem(this,sizeof(Chan)); }
sInt GetPeriod(sInt offs=0, sInt fineoffs=0)
{
sInt ft=FineTune+fineoffs;
while (ft>7) { 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
if (c.FXBuf[4]&0x0f) c.SetPeriod(0,VibTable[c.VibWave][(c.FXBuf[4]&0x0f)-1][c.VibPos]);
break;
case 7: // tremolo
if (c.FXBuf[7]&0x0f) TremVol=VibTable[c.TremWave][(c.FXBuf[7]&0x0f)-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.VibRetr=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<np)
c.Period=sMin(c.Period+c.FXBuf[3],np);
}
break;
case 6: // vibrato plus volslide
if (c.FXBuf[6]&0xf0)
c.Volume=sMin(c.Volume+(c.FXBuf[6]>>4),0x40);
else
c.Volume=sMax(c.Volume-(c.FXBuf[6]&0x0f),0);
// no break!
case 4: // vibrato ???
if (c.FXBuf[4]&0x0f) c.SetPeriod(0,VibTable[c.VibWave][(c.FXBuf[4]&0x0f)-1][c.VibPos]);
c.VibPos=(c.VibPos+(c.FXBuf[4]>>4))&0x3f;
break;
case 7: // tremolo ???
if (c.FXBuf[7]&0x0f) TremVol=VibTable[c.TremWave][(c.FXBuf[7]&0x0f)-1][c.TremPos];
c.TremPos=(c.TremPos+(c.FXBuf[7]>>4))&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 (c.LoopCount<fxpl)
{
CurRow=c.LoopStart-1;
c.LoopCount++;
}
else
c.LoopCount=0;
break;
case 9: // retrigger
if (++c.RetrigCount == c.FXBuf14[9])
{
c.RetrigCount=0;
TrigNote(ch,e);
}
break;
case 12: // cut
if (CurTick==c.FXBuf14[12])
c.Volume=0;
break;
case 13: // delay
if (CurTick==c.FXBuf14[13])
TrigNote(ch,e);
break;
}
break;
}
}
v.Volume=sClamp(c.Volume+TremVol,0,64);
v.Period=c.Period;
}
CurTick++;
if (CurTick>=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 '4LTF': case '!K!M':
SampleCount=32;
break;
}
if (SampleCount>16)
moddata+=(SampleCount-16)*sizeof(Sample);
for (sInt i=1; i<SampleCount; i++) Samples[i].Prepare();
PositionCount=*moddata; moddata+=2; // + skip unused byte
memcpy(PatternList,moddata,128); moddata+=128;
if (SampleCount>15) 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<PatternCount; i++)
{
Patterns[i].Load(moddata);
moddata+=1024;
}
sZeroMem(SData,sizeof(SData));
for (sInt i=1; i<SampleCount; i++)
{
SData[i]=(sS8*)moddata;
moddata+=2*Samples[i].Length;
}
Reset();
}
sU32 Render(sF32 *buf, sU32 len)
{
while (len)
{
sInt todo=sMin<sInt>(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];
//------------------------------------------------------------------------------
int main(int argc, char* argv[])
{
FILE *f;
fopen_s(&f,"c:\\mod\\dynasong.mod","rb");
fseek(f,0,SEEK_END);
sInt size=ftell(f);
fseek(f,0,SEEK_SET);
sU8 *mod = new sU8[size];
fread(mod,size,1,f);
fclose(f);
Paula P;
ModPlayer player(&P,mod);
dsInit(player.RenderProxy,&player,GetForegroundWindow());
MessageBox(0,player.Name,"TinyMOD",MB_OK);
dsClose();
delete[] mod;
return 0;
}
+788
View File
@@ -0,0 +1,788 @@
/*
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 <math.h>
#include <string.h>
#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<typename T> inline T sMin(const T a, const T b) { return (a<b)?a:b; }
template<typename T> inline T sMax(const T a, const T b) { return (a>b)?a:b; }
template<typename T> inline T sClamp(const T x, const T min, const T max) { return sMax(min,sMin(max,x)); }
template<typename T> T sSqr(T v) { return v*v; }
template<typename T> T sLerp(T a, T b, sF32 f) { return a+f*(b-a); }
template<typename T> 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; i<samples; i++)
{
if (!DivCnt)
{
// todo: use a fake d/a table for this
Cur.U32=((smp[Pos]^0x80)<<15)|0x40000000;
Cur.F32-=3.0f;
if (++Pos==SampleLen) Pos-=LoopLen;
DivCnt=Period;
}
if (PWMCnt<Volume) buffer[i]+=Cur.F32;
PWMCnt=(PWMCnt+1)&0x3f;
DivCnt--;
}
}
void Trigger(sS8 *smp,sInt sl, sInt ll, sInt offs=0)
{
Sample=smp;
SampleLen=sl;
LoopLen=ll;
Pos=sMin(offs,SampleLen-1);
}
};
Voice V[4];
// rendering in paula freq
static const sInt RBSIZE = 4096;
sF32 RingBuf[2*RBSIZE];
sInt WritePos;
sInt ReadPos;
sF32 ReadFrac;
void CalcFrag(sF32 *out, sInt samples)
{
sZeroMem(out,sizeof(sF32)*samples);
sZeroMem(out+RBSIZE,sizeof(sF32)*samples);
for (sInt i=0; i<4; i++)
{
if (i==1 || i==2)
V[i].Render(out+RBSIZE,samples);
else
V[i].Render(out,samples);
}
}
void Calc()
{
sInt RealReadPos=ReadPos-FIR_WIDTH-1;
sInt samples=(RealReadPos-WritePos)&(RBSIZE-1);
sInt todo=sMin(samples,RBSIZE-WritePos);
CalcFrag(RingBuf+WritePos,todo);
if (todo<samples)
{
WritePos=0;
todo=samples-todo;
CalcFrag(RingBuf,todo);
}
WritePos+=todo;
};
sF32 MasterVolume;
sF32 MasterSeparation;
// rendering in output freq
void Render(sF32 *outbuf, sInt samples)
{
const sF32 step=sF32(PAULARATE)/sF32(OUTRATE);
const sF32 pan=0.5f+0.5f*MasterSeparation;
const sF32 vm0=MasterVolume*sFSqrt(pan);
const sF32 vm1=MasterVolume*sFSqrt(1-pan);
for (sInt s=0; s<samples; s++)
{
sInt ReadEnd=ReadPos+FIR_WIDTH+1;
if (WritePos<ReadPos) ReadEnd-=RBSIZE;
if (ReadEnd>WritePos) 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 (d<bestd)
{
bestd=d;
e.Note=i;
}
}
ptr+=4;
}
}
};
Sample *Samples;
sS8 *SData[32];
sInt SampleCount;
sInt ChannelCount;
sU8 PatternList[128];
sInt PositionCount;
sInt PatternCount;
Pattern Patterns[128];
struct Chan
{
sInt Note;
sInt Period;
sInt Sample;
sInt FineTune;
sInt Volume;
sInt FXBuf[16];
sInt FXBuf14[16];
sInt LoopStart;
sInt LoopCount;
sInt RetrigCount;
sInt VibWave;
sInt VibRetr;
sInt VibPos;
sInt VibAmpl;
sInt VibSpeed;
sInt TremWave;
sInt TremRetr;
sInt TremPos;
sInt TremAmpl;
sInt TremSpeed;
Chan() { sZeroMem(this,sizeof(Chan)); }
sInt GetPeriod(sInt offs=0, sInt fineoffs=0)
{
sInt ft=FineTune+fineoffs;
while (ft>7) { 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<np)
c.Period=sMin(c.Period+c.FXBuf[3],np);
}
break;
case 6: // vibrato plus volslide
if (c.FXBuf[6]&0xf0)
c.Volume=sMin(c.Volume+(c.FXBuf[6]>>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<fxpl)
{
CurRow=c.LoopStart-1;
c.LoopCount++;
}
else
c.LoopCount=0;
}
break;
case 9: // retrigger
if (++c.RetrigCount == c.FXBuf14[9])
{
c.RetrigCount=0;
TrigNote(ch,e);
}
break;
case 12: // cut
if (CurTick==c.FXBuf14[12])
c.Volume=0;
break;
case 13: // delay
if (CurTick==c.FXBuf14[13])
TrigNote(ch,e);
break;
}
break;
}
}
v.Volume=sClamp(c.Volume+TremVol,0,64);
v.Period=c.Period;
}
CurTick++;
if (CurTick>=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; i<SampleCount; i++) Samples[i].Prepare();
PositionCount=*moddata; moddata+=2; // + skip unused byte
memcpy(PatternList,moddata,128); moddata+=128;
if (SampleCount>15) 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<PatternCount; i++)
{
Patterns[i].Load(moddata);
moddata+=1024;
}
sZeroMem(SData,sizeof(SData));
for (sInt i=1; i<SampleCount; i++)
{
SData[i]=(sS8*)moddata;
moddata+=2*Samples[i].Length;
}
Reset();
}
sU32 Render(sF32 *buf, sU32 len)
{
while (len)
{
sInt todo=sMin<sInt>(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 <windows.h>
#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 <mod name>","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;
}
+1
View File
@@ -0,0 +1 @@
View File
View File
View File
View File