Skip to content

Commit

Permalink
Fix loading empty patterns in XM player.
Browse files Browse the repository at this point in the history
Use PAL frequencies for period calculation in XM player.
Minor cleaning up of old comment lines.
  • Loading branch information
Jani Halme committed Nov 24, 2015
1 parent edef61c commit b01be14
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 37 deletions.
32 changes: 19 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
webaudio-mod-player
===================

This is a MOD/S3M/XM module player implemented in JavaScript using the Web Audio API and runs fully within the browser.

The MOD player unit supports standard 4-channel Amiga Protracker modules, as well as 6- and 8-channel PC FastTracker modules.
Multichannel modules also work, although only mod.dope ('28CH') has been tested with. It also supports the Amiga "LED" lowpass
filter and most Protracker effects (although some bugs still remain).

The S3M player unit supports songs made in all versions of Scream Tracker 3. It performs mixing with a wider dynamic range
and uses "soft clipping" to roll off audio peaks without harsh limiting. Samples are interpolated with 32 bit floating
points for a very soft Ultrasoundish feel. Clicks from changing volume, looped samples and sample offset commands are being
mitigated using short ramps.

The XM player unit is currently under development so several features are not yet implemented and the player has many
bugs. It is not yet recommended to use the XM player in "production".
This is a MOD/S3M/XM module player implemented in Javascript using the Web Audio API and runs fully within the browser. It
has been tested and confirmed to work on Chrome 14+, Firefox 24+, Safari 6+ and Edge 20+. The Javascript performance of
the browsers varies significantly, so some modules may stutter on one browser while the same module can play flawlessly
on other ones. YMMV.

Although internally each file format is handled by a format specific player class, a front-end wrapper class is used to
provide a common programming interface for the player.

All player classes use 32-bit floating point arithmetic in the channel mixing code, as well as a wide dynamic range. The
output is scaled down to [-1, 1] domain using a "soft clipping" algorithm to roll off any audio peaks without harsh-sounding
limiting. This should - in most cases - produce a reasonably constant audio volume for all modules.

Additionally, S3M and XM player classes use linear sample interpolation and volume ramping to produce a smooth Gravis
Ultrasound -like sound quality. The MOD player class attempts to sound more like an Amiga by allowing audio aliasing and
applying a low pass filter.

None of the player classes fully implement all the features and effects in each file format, but all the major ones should
be implemented. In addition, there most certainly will be some playback bugs in each player class - let me know if you run
into some bad ones.

You can test the player here:

Expand Down
44 changes: 23 additions & 21 deletions js/ft2.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
todo:
- sample sequences in te-2rx.xm, little_man.xm and yuki_satellites.xm play off sync
- weird pattern data at pos 0x33 and 0x35 of aliensex.xm
- fix clicks - ramping isn's always working as intended
- implement missing volume column effect commands
- implement missing ft2 commands
Expand Down Expand Up @@ -284,28 +283,26 @@ Fasttracker.prototype.parse = function(buffer)
}
maxpatt++;

// allocate pattern data and initialize them all
// allocate arrays for pattern data
this.pattern=new Array(maxpatt);
this.patternlen=new Array(maxpatt);
for(i=0;i<maxpatt;i++) {
// initialize the pattern to defaults prior to unpacking
this.patternlen[i]=64;
this.pattern[i]=new Uint8Array(this.channels*this.patternlen[i]*5);
for(row=0;row<this.patternlen[i];row++) for(ch=0;ch<this.channels;ch++) {
this.pattern[i][row*this.channels*5 + ch*5 + 0]=255; // note (255=no note)
this.pattern[i][row*this.channels*5 + ch*5 + 1]=0; // instrument
this.pattern[i][row*this.channels*5 + ch*5 + 2]=255 // volume
this.pattern[i][row*this.channels*5 + ch*5 + 3]=255; // command
this.pattern[i][row*this.channels*5 + ch*5 + 4]=0; // parameter
}
}

// load and unpack patterns
offset+=hdrlen; // initial offset for patterns
i=0;
while(i<this.patterns) {
this.patternlen[i]=le_word(buffer, offset+5);
this.pattern[i]=new Uint8Array(this.channels*this.patternlen[i]*5);

// initialize pattern to defaults prior to unpacking
for(k=0;k<(this.patternlen[i]*this.channels);k++) {
this.pattern[i][k*5 + 0]=0; // note
this.pattern[i][k*5 + 1]=0; // instrument
this.pattern[i][k*5 + 2]=0; // volume
this.pattern[i][k*5 + 3]=0; // command
this.pattern[i][k*5 + 4]=0; // parameter
}

datalen=le_word(buffer, offset+7);
offset+=le_dword(buffer, offset); // jump over header
j=0; k=0;
Expand All @@ -326,6 +323,10 @@ Fasttracker.prototype.parse = function(buffer)
this.pattern[i][k+3]=buffer[offset+j++];
this.pattern[i][k+4]=buffer[offset+j++];
}
k+=5;
}

for(k=0;k<(this.patternlen[i]*this.channels*5);k+=5) {
// remap note to st3-style, 255=no note, 254=note off
if (this.pattern[i][k+0]==97) {
this.pattern[i][k+0]=254;
Expand All @@ -341,9 +342,10 @@ Fasttracker.prototype.parse = function(buffer)
// remap volume column setvol to 0x00..0x40, tone porta to 0x50..0x5f and 0xff for nop
if (this.pattern[i][k+2]<0x10) { this.pattern[i][k+2]=0xff; }
else if (this.pattern[i][k+2]>=0x10 && this.pattern[i][k+2]<=0x50) { this.pattern[i][k+2]-=0x10; }
else if (this.pattern[i][k+2]>=0xf0) this.pattern[i][k+2]-=0xa0; //(this.pattern[i][k+2]&0x0f)|0x50;
k+=5;
else if (this.pattern[i][k+2]>=0xf0) this.pattern[i][k+2]-=0xa0;
}

// unpack next pattern
offset+=j;
i++;
}
Expand Down Expand Up @@ -534,9 +536,9 @@ Fasttracker.prototype.calcperiod = function(mod, note, finetune) {
var p1=mod.periodtable[ 8 + (note%12)*8 + ft ];
var p2=mod.periodtable[ 8 + (note%12)*8 + ft + 1];
ft=(finetune/16.0) - ft;
pv=((1.0-ft)*p1 + ft*p2)*( 16.0/Math.pow(2, Math.floor(note/12)-1) ); // todo: why does octave need -1 to sound correct?
pv=((1.0-ft)*p1 + ft*p2)*( 16.0/Math.pow(2, Math.floor(note/12)-1) );
} else {
pv=10*12*16*4 - note*16*4 - finetune/2;
pv=7680.0 - note*64.0 - finetune/2;
}
return pv;
}
Expand All @@ -545,7 +547,7 @@ Fasttracker.prototype.calcperiod = function(mod, note, finetune) {

// advance player by a tick
Fasttracker.prototype.advance = function(mod) {
mod.stt=Math.floor(mod.samplerate/(mod.bpm*0.4));
mod.stt=Math.floor((125.0/mod.bpm) * (1/50.0)*mod.samplerate); // 50Hz

// advance player
mod.tick++;
Expand Down Expand Up @@ -746,9 +748,9 @@ Fasttracker.prototype.process_tick = function(mod) {
{
var f;
if (mod.amigaperiods) {
f=8363.0 * 1712.0/mod.channel[ch].voiceperiod;
f=8287.137 * 1712.0/mod.channel[ch].voiceperiod;
} else {
f=8363.0 * Math.pow(2.0, (6*12*16*4 - mod.channel[ch].voiceperiod) / (12*16*4));
f=8287.137 * Math.pow(2.0, (4608.0 - mod.channel[ch].voiceperiod) / 768.0);
}
mod.channel[ch].samplespeed=f/mod.samplerate;
}
Expand Down
5 changes: 2 additions & 3 deletions js/st3.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,7 @@ Screamtracker.prototype.parse = function(buffer)
}

i=0;
while(buffer[i] && i<0x1c)
this.title+=dos2utf(buffer[i++]); //String.fromCharCode(buffer[i++]);
while(buffer[i] && i<0x1c) this.title+=dos2utf(buffer[i++]);

this.ordNum=buffer[0x0020]|(buffer[0x0021]<<8);
this.insNum=buffer[0x0022]|(buffer[0x0023]<<8);
Expand Down Expand Up @@ -304,7 +303,7 @@ Screamtracker.prototype.parse = function(buffer)
j=0;
this.sample[i].name="";
while(buffer[offset+0x0030+j] && j<28) {
this.sample[i].name+=dos2utf(buffer[offset+0x0030+j]); //String.fromCharCode(buffer[offset+0x0030+j]);
this.sample[i].name+=dos2utf(buffer[offset+0x0030+j]);
j++;
}
this.sample[i].length=buffer[offset+0x10]|buffer[offset+0x11]<<8;
Expand Down

0 comments on commit b01be14

Please sign in to comment.