Compare commits
9
Commits
refactored
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e5ea9e7c5 | ||
|
|
82a41e1b30 | ||
|
|
17164b02d5 | ||
|
|
4c6ba830e6 | ||
|
|
5df9049ee6 | ||
|
|
e60184d043 | ||
|
|
5099e60803 | ||
|
|
dad35bf491 | ||
|
|
167f0b7c46 |
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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 Amiga’s sound chip, Paula, was able to modify a sound’s 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 that’s incremented every cycle and if its value is above the set volume, the voice is silenced for that cycle. So effectively it’s PWM with a pulse frequency of about 50Khz.
|
||||||
|
|
||||||
|
“But wait, shouldn’t 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 didn’t 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 we’re at it, let’s see if we can hack up a simple .MOD player too without using anything a 68000 didn’t 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 hadn’t 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 :)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Reference in New Issue
Block a user