electronoora / webaudio-mod-player

MOD/S3M/XM module player for Web Audio
https://mod.haxor.fi/
MIT License
373 stars 53 forks source link

Better integration in other environments plus minor improvements #26

Open jlbocquet opened 3 years ago

jlbocquet commented 3 years ago

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; }

  asset.state="ready";
  asset.loading=false;
  asset.onReady();
  if (asset.autostart) asset.play();
} else {
  asset.state="error";
  asset.loading=false;
}

} 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);

if (this.player.paused) {
  this.player.paused=false;
  return true;
}

this.endofsong=false;
this.player.endofsong=false;
this.player.paused=false;
this.player.initialize();
this.player.flags=1+2;
this.player.playing=true;
this.playing=true;

this.chvu=new Float32Array(this.player.channels);
for(i=0;i<this.player.channels;i++) this.chvu[i]=0.0;

this.onPlay();

this.player.delayfirst=this.bufferstodelay;
return true;

} 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;

var bufs=new Array(ape.outputBuffer.getChannelData(0), ape.outputBuffer.getChannelData(1));
var buflen=ape.outputBuffer.length;
mod.player.mix(mod.player, bufs, buflen);

// apply stereo separation and soft clipping
var outp=new Float32Array(2);
for(var s=0;s<buflen;s++) {
  outp[0]=bufs[0][s];
  outp[1]=bufs[1][s];

  // a more headphone-friendly stereo separation
  if (mod.separation) {
    t=outp[0];
    if (mod.separation==2) { // mono
      outp[0]=outp[0]*0.5 + outp[1]*0.5;
      outp[1]=outp[1]*0.5 + t*0.5;
    } else { // narrow stereo
      outp[0]=outp[0]*0.65 + outp[1]*0.35;
      outp[1]=outp[1]*0.65 + t*0.35;
    }
  }

  // scale down and soft clip
  outp[0]/=mod.mixval; outp[0]=0.5*(Math.abs(outp[0]+0.975)-Math.abs(outp[0]-0.975));
  outp[1]/=mod.mixval; outp[1]=0.5*(Math.abs(outp[1]+0.975)-Math.abs(outp[1]-0.975));

  bufs[0][s]=outp[0];
  bufs[1][s]=outp[1];
}

mod.row=mod.player.row;
mod.position=mod.player.position;
mod.speed=mod.player.speed;
mod.bpm=mod.player.bpm;
mod.endofsong=mod.player.endofsong;

if (mod.player.filter != mod.filter) {
  mod.setfilter(mod.player.filter);
}

if (mod.endofsong && mod.playing) mod.stop();

if (mod.delayfirst>0) mod.delayfirst--;
mod.delayload=0;

// update this.chvu from player channel vu
if (mod.playing && ! mod.paused) { // @@@ JLB
    for(var i=0;i<mod.player.channels;i++) {
      mod.chvu[i]=mod.chvu[i]*0.25 + mod.player.chvu[i]*0.75;    
      mod.player.chvu[i]=0.0;
    }
  } else {
    for(var i=0;i<mod.player.channels;i++) {
        mod.chvu[i] = 0;
    }
  }

}

}