// read-only data from player class
this.title="";
this.signature="....";
this.songlen=0;
this.channels=0;
this.patterns=0;
this.samplenames=new Array();
// @@@ JLB
this.createContext();
}
// load module from url into local buffer
Modplayer.prototype.load = function(url)
{
// try to identify file format from url and create a new
// player class for it
this.url=url;
var ext=url.split('.').pop().toLowerCase().trim();
if (this.supportedformats.indexOf(ext)==-1) {
// unknown extension, maybe amiga-style prefix?
ext=url.split('/').pop().split('.').shift().toLowerCase().trim();
if (this.supportedformats.indexOf(ext)==-1) {
// ok, give up
return false;
}
}
this.format=ext;
switch (ext) {
case 'mod':
this.player=new Protracker();
break;
case 's3m':
this.player=new Screamtracker();
break;
case 'xm':
this.player=new Fasttracker();
break;
}
this.player.onReady=this.loadSuccess;
this.state="loading..";
var request = new XMLHttpRequest();
request.open("GET", this.url, true);
request.responseType = "arraybuffer";
this.request = request;
this.loading=true;
var asset = this;
request.onprogress = function(oe) {
asset.state="loading ("+Math.floor(100*oe.loaded/oe.total)+"%)..";
};
request.onload = function() {
var buffer=new Uint8Array(request.response);
this.state="parsing..";
if (asset.player.parse(buffer)) {
// copy static data from player
asset.title=asset.player.title
asset.signature=asset.player.signature;
asset.songlen=asset.player.songlen;
asset.channels=asset.player.channels;
asset.patterns=asset.player.patterns;
asset.filter=asset.player.filter;
if (asset.context) asset.setfilter(asset.filter);
asset.mixval=asset.player.mixval; // usually 8.0, though
asset.samplenames=new Array(32)
for(i=0;i<32;i++) asset.samplenames[i]="";
if (asset.format=='xm' || asset.format=='it') {
for(i=0;i<asset.player.instrument.length;i++) asset.samplenames[i]=asset.player.instrument[i].name;
} else {
for(i=0;i<asset.player.sample.length;i++) asset.samplenames[i]=asset.player.sample[i].name;
}
// play loaded and parsed module with webaudio context
Modplayer.prototype.play = function()
{
if (this.loading) return false;
if (this.player) {
// @@@ JLB if (this.context == null) this.createContext();
this.player.samplerate=this.samplerate;
if (this.context) this.setfilter(this.player.filter);
// are there E8x sync events queued?
Modplayer.prototype.hassyncevents = function()
{
if (this.player) return (this.player.syncqueue.length != 0);
return false;
}
// pop oldest sync event nybble from the FIFO queue
Modplayer.prototype.popsyncevent = function()
{
if (this.player) return this.player.syncqueue.pop();
}
// ger current pattern number
Modplayer.prototype.currentpattern = function()
{
if (this.player) return this.player.patterntable[this.player.position];
// @@@ JLB
return 0;
}
// get current pattern in standard unpacked format (note, sample, volume, command, data)
// note: 254=noteoff, 255=no note
// sample: 0=no instrument, 1..255=sample number
// volume: 255=no volume set, 0..64=set volume, 65..239=ft2 volume commands
// command: 0x2e=no command, 0..0x24=effect command
// data: 0..255
Modplayer.prototype.patterndata = function(pn)
{
var i, c, patt;
if (this.format=='mod') {
patt=new Uint8Array(this.player.pattern_unpack[pn]);
for(i=0;i<64;i++) for(c=0;c<this.player.channels;c++) {
if (patt[i5this.channels+c5+3]==0 && patt[i5this.channels+c5+4]==0) {
patt[i5this.channels+c5+3]=0x2e;
} else {
patt[i5this.channels+c5+3]+=0x37;
if (patt[i5this.channels+c5+3]<0x41) patt[i5this.channels+c5+3]-=0x07;
}
}
} else if (this.format=='s3m') {
patt=new Uint8Array(this.player.pattern[pn]);
for(i=0;i<64;i++) for(c=0;c<this.player.channels;c++) {
if (patt[i5this.channels+c5+3]==255) patt[i5this.channels+c5+3]=0x2e;
else patt[i5this.channels+c5+3]+=0x40;
}
} else if (this.format=='xm') {
patt=new Uint8Array(this.player.pattern[pn]);
for(i=0;i<this.player.patternlen[pn];i++) for(c=0;c<this.player.channels;c++) {
if (patt[i5this.channels+c5+0]<97)
patt[i5this.channels+c5+0]=(patt[i5this.channels+c5+0]%12)|(Math.floor(patt[i5this.channels+c5+0]/12)<<4);
if (patt[i5this.channels+c5+3]==255) patt[i5this.channels+c5+3]=0x2e;
else {
if (patt[i5this.channels+c5+3]<0x0a) {
patt[i5this.channels+c5+3]+=0x30;
} else {
patt[i5this.channels+c5+3]+=0x41-0x0a;
}
}
}
}
return patt;
}
// check if a channel has a note on
Modplayer.prototype.noteon = function(ch)
{
if (ch>=this.channels) return 0;
return this.player.channel[ch].noteon;
}
// get currently active sample on channel
Modplayer.prototype.currentsample = function(ch)
{
if (ch>=this.channels) return 0;
if (this.format=="xm" || this.format=="it") return this.player.channel[ch].instrument;
return this.player.channel[ch].sample;
}
// get length of currently playing pattern
Modplayer.prototype.currentpattlen = function()
{
if (this.format=="mod" || this.format=="s3m") return 64;
return this.player.patternlen[this.player.patterntable[this.player.position]];
}
// create the web audio context
Modplayer.prototype.createContext = function()
{
// @@@ JLB
if (this.context === null || this.context === undefined) {
if ( typeof AudioContext !== 'undefined') {
this.context = new AudioContext();
} else {
this.context = new webkitAudioContext();
}
}
File : player.js Changes are identified with "@@@ JLB" See my integration at https://webaudiostudio.jlbee.fr
/ front end wrapper class for format-specific player classes (c) 2015-2017 firehawk/tda /
// @@@ JLB : added 2 optionnal parameters function Modplayer(audioContext, bConnectToDest) { this.supportedformats=new Array('mod', 's3m', 'xm');
this.url=""; this.format="s3m";
this.state="standby"; this.request=null;
this.loading=false; this.playing=false; this.paused=false; this.repeat=false;
this.separation=1; this.mixval=8.0;
this.amiga500=false;
this.filter=false; this.endofsong=false;
this.autostart=false; this.bufferstodelay=4; // adjust this if you get stutter after loading new song this.delayfirst=0; this.delayload=0;
this.onReady=function(){}; this.onPlay=function(){}; this.onStop=function(){};
this.buffer=0; this.mixerNode=0;
// @@@ JLB this.bConnectToDest = bConnectToDest; this.context=audioContext;
this.samplerate=44100; this.bufferlen=4096;
this.chvu=new Float32Array(32);
// format-specific player this.player=null;
// read-only data from player class this.title=""; this.signature="...."; this.songlen=0; this.channels=0; this.patterns=0; this.samplenames=new Array();
// @@@ JLB this.createContext(); }
// load module from url into local buffer Modplayer.prototype.load = function(url) { // try to identify file format from url and create a new // player class for it this.url=url; var ext=url.split('.').pop().toLowerCase().trim(); if (this.supportedformats.indexOf(ext)==-1) { // unknown extension, maybe amiga-style prefix? ext=url.split('/').pop().split('.').shift().toLowerCase().trim(); if (this.supportedformats.indexOf(ext)==-1) { // ok, give up return false; } } this.format=ext;
switch (ext) { case 'mod': this.player=new Protracker(); break; case 's3m': this.player=new Screamtracker(); break; case 'xm': this.player=new Fasttracker(); break; }
this.player.onReady=this.loadSuccess;
this.state="loading.."; var request = new XMLHttpRequest(); request.open("GET", this.url, true); request.responseType = "arraybuffer"; this.request = request; this.loading=true; var asset = this; request.onprogress = function(oe) { asset.state="loading ("+Math.floor(100*oe.loaded/oe.total)+"%).."; }; request.onload = function() { var buffer=new Uint8Array(request.response); this.state="parsing.."; if (asset.player.parse(buffer)) { // copy static data from player asset.title=asset.player.title asset.signature=asset.player.signature; asset.songlen=asset.player.songlen; asset.channels=asset.player.channels; asset.patterns=asset.player.patterns; asset.filter=asset.player.filter; if (asset.context) asset.setfilter(asset.filter); asset.mixval=asset.player.mixval; // usually 8.0, though asset.samplenames=new Array(32) for(i=0;i<32;i++) asset.samplenames[i]=""; if (asset.format=='xm' || asset.format=='it') { for(i=0;i<asset.player.instrument.length;i++) asset.samplenames[i]=asset.player.instrument[i].name; } else { for(i=0;i<asset.player.sample.length;i++) asset.samplenames[i]=asset.player.sample[i].name; }
} request.send(); return true; }
// play loaded and parsed module with webaudio context Modplayer.prototype.play = function() { if (this.loading) return false; if (this.player) { // @@@ JLB if (this.context == null) this.createContext(); this.player.samplerate=this.samplerate; if (this.context) this.setfilter(this.player.filter);
} else { return false; } }
// pause playback Modplayer.prototype.pause = function() { if (this.player) { if (!this.player.paused) { this.player.paused=true; } else { this.player.paused=false; } } }
// stop playback Modplayer.prototype.stop = function() { this.paused=false; this.playing=false; if (this.player) { this.player.paused=false; this.player.playing=false; this.player.delayload=1; } this.onStop(); }
// stop playing but don't call callbacks Modplayer.prototype.stopaudio = function(st) { if (this.player) { this.player.playing=st; } }
// jump positions forward/back Modplayer.prototype.jump = function(step) { if (this.player) { this.player.tick=0; this.player.row=0; this.player.position+=step; this.player.flags=1+2; if (this.player.position<0) this.player.position=0; if (this.player.position >= this.player.songlen) this.stop(); } this.position=this.player.position; this.row=this.player.row; }
// @@@ JLB Modplayer.prototype.getPercentPlayed = function() { if (this.player === null) return 0; var totalstep = this.player.songlen 64; var curstep = this.player.position 64 + this.player.row; if (totalstep === 0) return 0; return curstep * 100 / totalstep; }
// set whether module repeats after songlen Modplayer.prototype.setrepeat = function(rep) { this.repeat=rep; if (this.player) this.player.repeat=rep; }
// set stereo separation mode (0=standard, 1=65/35 mix, 2=mono) Modplayer.prototype.setseparation = function(sep) { this.separation=sep; if (this.player) this.player.separation=sep; }
// set autostart to play immediately after loading Modplayer.prototype.setautostart = function(st) { this.autostart=st; }
// set amiga model - changes lowpass filter state Modplayer.prototype.setamigamodel = function(amiga) { if (amiga=="600" || amiga=="1200" || amiga=="4000") { this.amiga500=false; if (this.filterNode) this.filterNode.frequency.value=22050; } else { this.amiga500=true; if (this.filterNode) this.filterNode.frequency.value=6000; } }
// amiga "LED" filter Modplayer.prototype.setfilter = function(f) { if (f) { this.lowpassNode.frequency.value=3275; } else { this.lowpassNode.frequency.value=28867; } this.filter=f; if (this.player) this.player.filter=f; }
// are there E8x sync events queued? Modplayer.prototype.hassyncevents = function() { if (this.player) return (this.player.syncqueue.length != 0); return false; }
// pop oldest sync event nybble from the FIFO queue Modplayer.prototype.popsyncevent = function() { if (this.player) return this.player.syncqueue.pop(); }
// ger current pattern number Modplayer.prototype.currentpattern = function() { if (this.player) return this.player.patterntable[this.player.position]; // @@@ JLB return 0; }
// get current pattern in standard unpacked format (note, sample, volume, command, data) // note: 254=noteoff, 255=no note // sample: 0=no instrument, 1..255=sample number // volume: 255=no volume set, 0..64=set volume, 65..239=ft2 volume commands // command: 0x2e=no command, 0..0x24=effect command // data: 0..255 Modplayer.prototype.patterndata = function(pn) { var i, c, patt; if (this.format=='mod') { patt=new Uint8Array(this.player.pattern_unpack[pn]); for(i=0;i<64;i++) for(c=0;c<this.player.channels;c++) { if (patt[i5this.channels+c5+3]==0 && patt[i5this.channels+c5+4]==0) { patt[i5this.channels+c5+3]=0x2e; } else { patt[i5this.channels+c5+3]+=0x37; if (patt[i5this.channels+c5+3]<0x41) patt[i5this.channels+c5+3]-=0x07; } } } else if (this.format=='s3m') { patt=new Uint8Array(this.player.pattern[pn]); for(i=0;i<64;i++) for(c=0;c<this.player.channels;c++) { if (patt[i5this.channels+c5+3]==255) patt[i5this.channels+c5+3]=0x2e; else patt[i5this.channels+c5+3]+=0x40; } } else if (this.format=='xm') { patt=new Uint8Array(this.player.pattern[pn]); for(i=0;i<this.player.patternlen[pn];i++) for(c=0;c<this.player.channels;c++) { if (patt[i5this.channels+c5+0]<97) patt[i5this.channels+c5+0]=(patt[i5this.channels+c5+0]%12)|(Math.floor(patt[i5this.channels+c5+0]/12)<<4); if (patt[i5this.channels+c5+3]==255) patt[i5this.channels+c5+3]=0x2e; else { if (patt[i5this.channels+c5+3]<0x0a) { patt[i5this.channels+c5+3]+=0x30; } else { patt[i5this.channels+c5+3]+=0x41-0x0a; } } } } return patt; }
// check if a channel has a note on Modplayer.prototype.noteon = function(ch) { if (ch>=this.channels) return 0; return this.player.channel[ch].noteon; }
// get currently active sample on channel Modplayer.prototype.currentsample = function(ch) { if (ch>=this.channels) return 0; if (this.format=="xm" || this.format=="it") return this.player.channel[ch].instrument; return this.player.channel[ch].sample; }
// get length of currently playing pattern Modplayer.prototype.currentpattlen = function() { if (this.format=="mod" || this.format=="s3m") return 64; return this.player.patternlen[this.player.patterntable[this.player.position]]; }
// create the web audio context Modplayer.prototype.createContext = function() { // @@@ JLB if (this.context === null || this.context === undefined) { if ( typeof AudioContext !== 'undefined') { this.context = new AudioContext(); } else { this.context = new webkitAudioContext(); } }
this.samplerate=this.context.sampleRate; this.bufferlen=(this.samplerate > 44100) ? 4096 : 2048;
// Amiga 500 fixed filter at 6kHz. WebAudio lowpass is 12dB/oct, whereas // older Amigas had a 6dB/oct filter at 4900Hz. this.filterNode=this.context.createBiquadFilter(); if (this.amiga500) { this.filterNode.frequency.value=6000; } else { this.filterNode.frequency.value=22050; }
// "LED filter" at 3275kHz - off by default this.lowpassNode=this.context.createBiquadFilter(); this.setfilter(this.filter);
// mixer if ( typeof this.context.createJavaScriptNode === 'function') { this.mixerNode=this.context.createJavaScriptNode(this.bufferlen, 1, 2); } else { this.mixerNode=this.context.createScriptProcessor(this.bufferlen, 1, 2); } this.mixerNode.module=this; this.mixerNode.onaudioprocess=Modplayer.prototype.mix;
// patch up some cables :) this.mixerNode.connect(this.filterNode); this.filterNode.connect(this.lowpassNode);
if (this.bConnectToDest === true) { // @@@ JLB this.lowpassNode.connect(this.context.destination); } }
// @@@ JLB Modplayer.prototype.getOutputNode = function() { return this.lowpassNode; }
// scriptnode callback - pass through to player class Modplayer.prototype.mix = function(ape) { var mod;
if (ape.srcElement) { mod=ape.srcElement.module; } else { mod=this.module; }
if (mod.player && mod.delayfirst==0) { mod.player.repeat=mod.repeat;
}
}