Skip to content

Instantly share code, notes, and snippets.

@django-wong
Last active February 8, 2022 16:49
Show Gist options
  • Save django-wong/cd3d8d6910a1d4aa9ce9 to your computer and use it in GitHub Desktop.
Save django-wong/cd3d8d6910a1d4aa9ce9 to your computer and use it in GitHub Desktop.
Mixer - Javascript
var Mixer = function(){
"use strict";
//Audio buffer
this.buffer = {};
//source delay
this.delay = [];
//when the play will start.
this.when = [];
//where the playback will start.
this.offset = [];
// the length of the portion of the asset to be played.
this.duration = [];
this.audio = {
sources : {},
effect : {}
};
this.playState = 'stop';
this.currentTime = undefined;
this.events = {};
this.orgin = [];
this.pauseTime = 0;
this.elapsed = 0;
this.loadState = '';
this.trackNum = 0;
this.playCount = 0;
this.AudioContext = new AudioContext();
this.init();
};
Mixer.prototype.buffer = {};
Mixer.prototype.audio = [];
Mixer.prototype.Analyser = {};
/**
* initialization
* @return {void}
*/
Mixer.prototype.init = function() {
this._initAnalyser_();
this.currentTime = 0;
};
Mixer.prototype._initAnalyser_ = function() {
this.Analyser = this.AudioContext.createAnalyser();
this.Analyser.BYTE_FREQUENCY = 1;
this.Analyser.BYTE_TIME_DOMAIN = 2;
this.Analyser.FLOAT_FREQUENCY = 3;
this.Analyser.FLOAT_TIME_DOMAIN = 4;
};
/**
* add a source
* @param {source} source
*/
Mixer.prototype.addSource = function(source, i){
this.audio.sources[i] = {
source : source,
on : 0
};
return;
};
/**
* delete a source
* @param {string} source_id
* @return {[type]} ·µ»Øsource
*/
Mixer.prototype.deleteSource = function(source_id){
delete this.audio.sources[source_id];
};
/**
* load Arraybuffer from url
* @param {string} url
* @param {Function} callback Call in the load progress is complete, you can realize a batch loader based on this function
* @return {void}
*/
Mixer.prototype.loadAudioFromUrl = function(url, callback){
var request = new XMLHttpRequest();
this.request = request;
var Mixer = this;
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onload = function() {
if(request.status !== 200){
Mixer.trigger('loadError', url, this);
}else{
callback(request.response, 'normal');
}
};
request.onprogress = function(event){
Mixer.trigger('loadProgress', event.total, event.loaded, this);
};
request.onabort = function(){
Mixer.trigger('loadCancel', url, this);
Mixer.trigger('finished');
callback = function(){};
console.log('cancel');
};
request.ontimeout = function(){
Mixer.trigger('loadError', url, this);
};
request.onerror = function(){
Mixer.trigger('loadError', url, this);
};
request.send();
};
/**
* create source
* @param {string|bolean} which Create source from which buffer, true mean create all
* @return {void}
*/
Mixer.prototype.createSource = function(which){
var b = this.buffer;
if(which && which!==true){
b=[];
b[which] = this.buffer[which];
}
for (var i in b) {
var source = this.AudioContext.createBufferSource();
source.buffer = b[i];
this.addSource(source, i);
}
};
/**
* play all buffer
* @return {void}
*/
Mixer.prototype.startAll = function(){
this.playState = 'playing';
this.currentTime = this.AudioContext.currentTime;
this._play(true);
this.trigger('playing');
};
/**
* stop and clean up sources
* @return {void}
*/
Mixer.prototype.stopAll = function(){
this.playState = 'stop';
this.trigger('stop');
this.elapsed=0;
this.Analyser.disconnect();
this._stop(true);
};
/**
* puase
* @return {void}
*/
Mixer.prototype.pauseAll = function() {
this.playState = 'pause';
this._stop(true);
this.pauseTime = this.getPlaybackProgress();
this.elapsed += (this.AudioContext.currentTime - this.currentTime);
this.trigger('pause');
};
Mixer.prototype.startFromPause = function(which) {
this.playState = 'playing';
this.currentTime = this.AudioContext.currentTime;
this._play(which, false, true);
this.trigger('playing');
};
Mixer.prototype.playCounter = function(which) {
if(this.playState === 'playing'){
this.trigger('end', which);
console.log('end: ', which);
}
};
/**
* Do play, this function should not participate in any actions about time counter, should not be called directly
* @param {string|boolean} Which one to play, true or empty means play all
* @param {boolean} play from the beginning ´ÓÍ·²¥·Å
* @return {void}
*/
Mixer.prototype._play = function(which, fromTheBeginning, fromPause) {
// this.command = 'play';
this.createSource(which);
for(var k in this.audio.sources){
var s = this.audio.sources[k];
s.source.connect(this.Analyser);
s.playState = 'playing';
this.Analyser.connect(this.AudioContext.destination);
this.when[k] = this.when[k]?Number(this.when[k]):0;
this.offset[k] = this.offset[k]?this.offset[k]:0;
this.duration[k] = this.duration[k]?this.duration[k]-this.offset[k]:this.buffer[k].duration-this.offset[k];
s.source.onended = this.playCounter.bind(this, k);
if(fromTheBeginning){
console.log('play over again, Skip the delay',this.when[k], this.offset[k], this.duration[k]);
s.source.start(this.when[k], this.offset[k], this.duration[k]);
}else if(fromPause){
var when = this.getPositivenumber(this.when[k]-this.getPlaybackProgress())+this.AudioContext.currentTime;
var offset= this.offset[k]+this.getPositivenumber(this.elapsed-this.when[k]);
var duration = this.getPositivenumber(this.duration[k]-offset);
if(duration === 0){
console.log('skip');
s.playState = 'stop';
s.source.start();
s.source.stop();
}else{
console.log(when, offset, duration);
s.source.start(when, offset, duration);
}
}else{
console.log(this.when[k], this.AudioContext.currentTime);
console.log('play '+k+' over again',this.when[k]+this.AudioContext.currentTime, this.offset[k]+this.elapsed, this.duration[k]-this.elapsed);
s.source.start(this.when[k]+this.AudioContext.currentTime, this.offset[k]+this.elapsed, this.duration[k]-this.elapsed);
}
}
};
/**
* Do stop, this function should not participate in any actions about time counter, should not be called directly
* @param {string|boolean} which on to stop, true or empty means stop all
* @return {void}
*/
Mixer.prototype._stop = function(which) {
// this.command = 'stop';
var source = this.audio.sources;
if(which && which!==true){
source = [];
source[which] = this.audio.sources[which];
}
for(var k in source){
var s = source[k];
if(s.playState === 'playing'){
s.source.connect(this.AudioContext.destination);
if(this.playState !== 'playing'){
//do not trigger end event
s.source.onended = function(){};
}
s.source.stop();
}
delete source[k];
}
//clear up source
this.audio.sources = [];
};
/**
* Batch buffer loader, trigger 'load' in complete
* @param {array} urlList
* @param {Function} call in complete
* @return {[type]}
*/
Mixer.prototype.loadBuffer = function(url, callback){
console.log(url);
var loadCount = 0;
var errorCount = 0;
var Mixer = this;
Mixer.loadState = 'loading';
Mixer.trigger('loading');
var buffer_id;
var current_buffer_id;
// console.log('+++', buffer_ids);
/**
* use for count loading progress, create and save buffer, trigger 'load' in complete.
* @param {buffer} buffer use for play source
* @return {void}
*/
var createrCallback = function(buffer){
Mixer.trigger('load');
if(buffer){
buffer_id = Mixer.addBuffer(buffer, current_buffer_id);
}else{
Mixer.trigger('loadError');
}
Mixer.loadState = 'finished';
if(buffer && callback){
callback(buffer_id, true);
}else if(callback){
callback(null, false);
}
};
/**
* create buffer
* @param {ArrayBuffer} Arraybuffer return by XMLHttpRequest, update counter when complete
* @return {[type]} [description]
*/
var creater = function(AudioData, status){
if(status === 'normal' && AudioData){
Mixer.createBuffer(AudioData, createrCallback);
}else{
createrCallback.call(false);
}
};
current_buffer_id = url.buffer_id || undefined;
this.loadAudioFromUrl(url.src, creater);
};
/**
* add buffer
* @param {string} buffer buffer_id
*/
Mixer.prototype.addBuffer = function(buffer, uuid) {
uuid = uuid || this.uuid();
this.buffer[uuid] = buffer;
return uuid;
};
Mixer.prototype.removeBuffer = function(buffer_id) {
delete this.buffer[buffer_id];
};
/**
* create source.buffer
* @param {ArrayBuffer}
* @return {void}
*/
Mixer.prototype.createBuffer = function(AudioData, callback) {
this.AudioContext.decodeAudioData(AudioData, callback);
};
/**
* get wave info
* @param {int} type
* @param {int} rate
* @return {byteArray || floatArray }
*/
Mixer.prototype.getAnalyser = function(type, rate){
if(rate === undefined){
rate = this.AudioContext.sampleRate;
}
var length = this.Analyser.frequencyBinCount*rate/this.AudioContext.sampleRate|0;
var output;
switch (type){
case this.Analyser.BYTE_FREQUENCY:
output = new Uint8Array(length);
this.Analyser.getByteFrequencyData(output);
break;
case this.Analyser.BYTE_TIME_DOMAIN:
output = new Uint8Array(length);
this.Analyser.getByteTimeDomainData(output);
break;
case this.Analyser.FLOAT_FREQUENCY:
output = new Float32Array(length);
this.Analyser.getFloatFrequencyData(output);
break;
case this.Analyser.FLOAT_TIME_DOMAIN:
output = new Float32Array(length);
this.Analyser.getFloatTimeDomainData(output);
break;
default:
output = new Uint8Array(length);
this.Analyser.getByteFrequencyData(output);
break;
}
return output;
};
/**
* get PCM data
* @param {string} buffer_id
* @return {Float32Array}
*/
Mixer.prototype.getChannelData = function(buffer_id) {
var data = [];
var buffer = this.buffer[buffer_id];
for (var i = 0; i < buffer.numberOfChannels; i++) {
data.push(buffer.getChannelData(i));
}
return data;
};
/**
* Gets the total playing time
* @return {Float}
*/
Mixer.prototype.getPlaybackProgress = function() {
if(this.playState !== 'playing'){
return this.elapsed;
}
return this.AudioContext.currentTime-this.currentTime+this.elapsed;
};
Mixer.prototype.setPlaybackProgress = function(s) {
if(this.playState === 'playing'){
console.info('tracks is playing, please call \'stop\' before set playback progress');
return;
}
this.elapsed = s;
};
/**
* get druation
* @param {string} buffer id
* @return {int}
*/
Mixer.prototype.getDuration = function(buffer_id) {
var buffer = this.buffer[buffer_id];
return buffer?buffer.duration:0;
};
/**
* register event callback
* @param {string} event name
* @param {Function} callback
* @return {void}
*/
Mixer.prototype.on = function(event, callback) {
if(!this.events[event]){
this.events[event] = [];
}
this.events[event].push(callback);
return this;
};
/**
* trigger
* @return {string}
*/
Mixer.prototype.trigger = function() {
var args = Array.prototype.slice.call(arguments);
var eventName = args.shift();
console.info('Mixer: '+eventName);
if(!this.events[eventName]){
this.events[eventName] = [];
}
var e = this.events[eventName];
for (var i = 0; i < e.length; i++) {
var f = e[i];
f.apply(this, args);
}
return this;
};
Mixer.prototype.unbind = function(eventName, callback){
if(this.events[eventName]){
for(var i in this.events[eventName]){
if(callback !== undefined){
if(this.events[eventName][i] === callback){
this.events[eventName].splice(i,1);
console.log('unbind callback', this.events[eventName]);
}else{
console.log('tring to unbind '+eventName+' callback, but not callback found');
}
}else{
delete this.events[eventName];
}
}
}
};
/**
* play buffer
* @param {string} buffer_id
* @return {void}
*/
Mixer.prototype.playOne = function(buffer_id) {
};
/**
* save sa wav file
* @return {void}
*/
Mixer.prototype.saveAsFile = function() {
};
Mixer.prototype.uuid = function(len, radix) {
var CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
.split("");
var chars = CHARS, uuid = [], i;
radix = radix || chars.length;
if (len) {
for (i = 0; i < len; i++)
uuid[i] = chars[0 | Math.random() * radix];
} else {
var r;
uuid[8] = uuid[13] = uuid[18] = uuid[23] = "-";
uuid[14] = "4";
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join("");
};
Mixer.prototype.getString = function() {
var p = {};
for(var i in this.buffer){
p[i] = {
src : 0,
when : this.when[i] || 0,
offset : this.offset[i] || 0,
duration : this.duration[i] || 0,
channelNum : this.buffer[i].numberOfChannels
};
}
return p;
};
/**
* clear all data for load a draft or other operation.
*/
Mixer.prototype.Refresh = function() {
this._stop(true);
//Audio buffer
this.buffer = {};
this.buffer_ids = [];
//source delay
this.delay = [];
//when the play will start.
this.when=[];
//where the playback will start.
this.offset=[];
// the length of the portion of the asset to be played.
this.duration=[];
this.audio = {
sources : [],
effect : {}
};
this.init();
this.playState = undefined;
this.currentTime = 0;
// this.events = {};
this.orgin = [];
this.pauseTime = 0;
this.elapsed=0;
this.loadState = '';
this.trackNum=0;
// this.AudioContext = new AudioContext();
// this.Analyser = this.AudioContext.createAnalyser();
};
Mixer.prototype.getPositivenumber = function(num){
if(num<0){
return 0;
}else{
return num;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment