webdaw-modules
Version:
a set of modules for building a web-based DAW
1,583 lines (1,332 loc) • 662 kB
JavaScript
var sequencer;
// import { version } from "../package.json";
var version = "0.0.25";
function openModule() {
"use strict";
var protectedScope,
initMethods = [],
webaudioUnlocked = false,
src,
context,
gainNode,
compressor,
sampleIndex = 0,
compressorParams = [
"threshold",
"knee",
"ratio",
"reduction",
"attack",
"release",
],
ua = navigator.userAgent,
os,
browser,
legacy = false;
if (ua.match(/(iPad|iPhone|iPod)/g)) {
os = "ios";
// webaudioUnlocked = false;
} else if (ua.indexOf("Android") !== -1) {
os = "android";
} else if (ua.indexOf("Linux") !== -1) {
os = "linux";
} else if (ua.indexOf("Macintosh") !== -1) {
os = "osx";
} else if (ua.indexOf("Windows") !== -1) {
os = "windows";
}
if (ua.indexOf("Chrome") !== -1) {
// chrome, chromium and canary
browser = "chrome";
if (ua.indexOf("OPR") !== -1) {
browser = "opera";
} else if (ua.indexOf("Chromium") !== -1) {
browser = "chromium";
}
} else if (ua.indexOf("Safari") !== -1) {
browser = "safari";
} else if (ua.indexOf("Firefox") !== -1) {
browser = "firefox";
} else if (ua.indexOf("Trident") !== -1) {
browser = "Internet Explorer";
}
if (os === "ios") {
if (ua.indexOf("CriOS") !== -1) {
browser = "chrome";
}
}
// console.log(os, browser, '---', ua);
if (window.AudioContext) {
context = new window.AudioContext();
if (typeof context.createGainNode !== "function") {
context.createGainNode = context.createGain;
}
} else if (window.webkitAudioContext) {
context = new window.webkitAudioContext();
if (typeof context.createGainNode !== "function") {
context.createGainNode = context.createGain;
}
} else {
//alert('Your browser does not support AudioContext!\n\nPlease use one of these browsers:\n\n- Chromium (Linux | Windows)\n- Firefox (OSX | Windows)\n- Chrome (Linux | Android | OSX | Windows)\n- Canary (OSX | Windows)\n- Safari (iOS 6.0+ | OSX)\n\nIf you use Chrome or Chromium, heartbeat uses the WebMIDI api');
throw new Error(
"The WebAudio API hasn't been implemented in " +
browser +
", please use any other browser"
);
}
compressor = context.createDynamicsCompressor();
compressor.connect(context.destination);
//console.log(compressor);
gainNode = context.createGainNode();
//gainNode.connect(compressor);
gainNode.connect(context.destination);
gainNode.gain.value = 1;
protectedScope = {
context: context,
//destination: context.destination,
masterGainNode: gainNode,
masterCompressor: compressor,
useDelta: false,
timedTasks: {},
scheduledTasks: {},
repetitiveTasks: {},
getSampleId: function () {
return "S" + sampleIndex++ + new Date().getTime();
},
addInitMethod: function (method) {
initMethods.push(method);
},
callInitMethods: function () {
var i,
maxi = initMethods.length;
for (i = 0; i < maxi; i++) {
initMethods[i]();
}
},
};
/**
@namespace sequencer
*/
sequencer = {
name: "qambi",
version,
protectedScope: protectedScope,
ui: {},
ua: ua,
os: os,
browser: browser,
legacy: false,
midi: false,
webmidi: false,
webaudio: true,
jazz: false,
ogg: false,
mp3: false,
record_audio: navigator.getUserMedia !== undefined,
bitrate_mp3_encoding: 128,
util: {},
debug: 0, // 0 = off, 1 = error, 2 = warn, 3 = info, 4 = log
defaultInstrument: "sinewave",
pitch: 440,
bufferTime: 350 / 1000, //seconds
autoAdjustBufferTime: false,
noteNameMode: "sharp",
minimalSongLength: 60000, //millis
pauseOnBlur: false,
restartOnFocus: true,
defaultPPQ: 960,
overrulePPQ: true,
precision: 3, // means float with precision 3, e.g. 10.437
midiInputs: {},
midiOutputs: {},
storage: {
midi: {
id: "midi",
},
audio: {
id: "audio",
recordings: {},
},
instruments: {
id: "instruments",
},
samplepacks: {
id: "samplepacks",
},
assetpacks: {
id: "assetpacks",
},
},
getAudioContext: function () {
return context;
},
getMasterGainNode: function () {
return gainNode;
},
getTime: function () {
return context.currentTime;
// return performance.now() / 1000;
},
getTimeDiff: function () {
var contextTime = context.currentTime * 1000;
return performance.now() - contextTime;
},
setMasterVolume: function (value) {
value = value < 0 ? 0 : value > 1 ? 1 : value;
gainNode.gain.value = value;
},
getMasterVolume: function () {
return gainNode.gain.value;
},
getCompressionReduction: function () {
//console.log(compressor);
return compressor.reduction.value;
},
enableMasterCompressor: function (flag) {
if (flag) {
gainNode.disconnect(0);
gainNode.connect(compressor);
compressor.disconnect(0);
compressor.connect(context.destination);
} else {
compressor.disconnect(0);
gainNode.disconnect(0);
gainNode.connect(context.destination);
}
},
configureMasterCompressor: function (cfg) {
/*
readonly attribute AudioParam threshold; // in Decibels
readonly attribute AudioParam knee; // in Decibels
readonly attribute AudioParam ratio; // unit-less
readonly attribute AudioParam reduction; // in Decibels
readonly attribute AudioParam attack; // in Seconds
readonly attribute AudioParam release; // in Seconds
*/
var i, param;
for (i = compressorParams.length; i >= 0; i--) {
param = compressorParams[i];
if (cfg[param] !== undefined) {
compressor[param].value = cfg[param];
}
}
},
unlockWebAudio: function () {
// console.log('unlock webaudio');
if (webaudioUnlocked === true) {
// console.log('already unlocked');
return;
}
if (typeof context.resume === "function") {
context.resume();
}
var src = context.createOscillator(),
gainNode = context.createGainNode();
gainNode.gain.value = 0;
src.connect(gainNode);
gainNode.connect(context.destination);
if (src.noteOn !== undefined) {
src.start = src.noteOn;
src.stop = src.noteOff;
}
src.start(0);
src.stop(0.001);
webaudioUnlocked = true;
},
};
// debug levels
Object.defineProperty(sequencer, "ERROR", { value: 1 });
Object.defineProperty(sequencer, "WARN", { value: 2 });
Object.defineProperty(sequencer, "INFO", { value: 3 });
Object.defineProperty(sequencer, "LOG", { value: 4 });
}
function assetManager() {
'use strict';
// console.log('AssetManager');
var
// import
loadLoop, //defined in util.js
findItem, //defined in util.js
storeItem, //defined in util.js
deleteItem, //defined in util.js
typeString, //defined in util.js
getArguments, //defined in util.js
isEmptyObject, // defined in util.js
objectForEach, //defined in util.js
storage, //defined in open_module.js
updateInstruments, //defined in sequencer.js
findItemsInFolder, //defined in util.js
busy = false,
taskIndex = 0,
finishedTasks = {},
taskQueue = [],
callbacks = [];
sequencer.removeMidiFile = function (path) {
var item,
items = [], i, folder;
if (path.className === 'MidiFile') {
item = path;
path = item.localPath;
} else {
item = findItem(path, storage.midi);
}
if (item.className === 'MidiFile') {
items.push(item);
} else {
folder = item;
objectForEach(folder, function (item) {
if (item.className === 'MidiFile') {
items.push(item);
}
});
}
for (i = items.length - 1; i >= 0; i--) {
item = items[i];
deleteItem(item.localPath, storage.midi);
}
};
sequencer.removeInstrument = function (path, unloadSamples) {
var item, items = [], i, folder, mapping, samplePath;
if (path.className === 'InstrumentConfig') {
item = path;
path = item.localPath;
} else {
item = findItem(path, storage.instruments);
}
if (item.className === 'InstrumentConfig') {
items.push(item);
} else {
folder = item;
for (i in folder) {
if (folder.hasOwnProperty(i)) {
item = folder[i];
if (item.className === 'InstrumentConfig') {
items.push(item);
}
}
}
}
for (i = items.length - 1; i >= 0; i--) {
item = items[i];
//console.log(item.mapping);
mapping = item.mapping;
samplePath = item.sample_path;
if (unloadSamples === true) {
// delete samples
objectForEach(mapping, function (value) {
deleteItem(samplePath + '/' + value.n, storage.audio);
});
// delete sample pack
deleteItem(samplePath, storage.samplepacks);
}
// remove instrument from storage
deleteItem(item.localPath, storage.instruments);
//return deleteItem(path, storage.instruments);
}
// if an instrument has been removed, inform the tracks that used that instrument
updateInstruments();
};
sequencer.removeSamplePack = function (path) {
var item,
items = [], i, samples, sample, s, folder;
if (path.className === 'SamplePack') {
item = path;
path = item.localPath;
} else {
item = findItem(path, storage.samplepacks);
}
if (item.className === 'SamplePack') {
items.push(item);
} else {
folder = item;
objectForEach(folder, function (item) {
if (item.className === 'SamplePack') {
items.push(item);
}
});
}
for (i = items.length - 1; i >= 0; i--) {
item = items[i];
//console.log(item.localPath);
samples = item.samples;
for (s = samples.length - 1; s >= 0; s--) {
sample = samples[s];
//console.log('->', sample.folder + '/' + sample.id);
deleteItem(sample.folder + '/' + sample.id, storage.audio);
}
item.reset();
deleteItem(item.localPath, storage.samplepacks);
}
updateInstruments();
/*
function loopInstruments(root){
var item;
for(i in root){
if(root.hasOwnProperty(i)){
if(i === 'id' || i === 'path' || i === 'className'){
continue;
}
item = root[i];
if(item.className === 'Folder'){
loopInstruments(item);
}else{
item = findItem(item.folder + '/' + item.name, storage.instruments);
console.log(item);
if(item.parse){
item.parse();
}
}
}
}
}
loopInstruments(storage.instruments);
*/
};
sequencer.removeAssetPack = function (path) {
var item,
folder;
if (path.className === 'AssetPack') {
item = path;
path = item.localPath;
} else {
item = findItem(path, storage.assetpacks);
}
if (item.className === 'AssetPack') {
item.unload();
} else {
folder = item;
objectForEach(folder, function (item) {
if (item.className === 'AssetPack') {
item.unload();
}
});
}
};
sequencer.startTaskQueue = function (cb) {
//console.log('startTaskQueue', taskQueue.length, busy);
if (busy === true) {
return;
}
busy = true;
loadQueueLoop(0, cb);
};
sequencer.addTask = function (task, callback, callbackAfterAllTasksAreDone) {
task.id = 'task' + taskIndex++;
taskQueue.push(task);
//console.log('task', task.type, taskQueue.length);
if (callback !== undefined) {
if (callbackAfterAllTasksAreDone === true) {
// call the callback only after all tasks are done
sequencer.addCallbackAfterTask(callback);
} else {
// call the callback right after this task is done
sequencer.addCallbackAfterTask(callback, [task.id]);
}
}
return task.id;
};
sequencer.addCallbackAfterTask = function (callback, taskIds) {
callbacks.push({
method: callback,
taskIds: taskIds
});
//console.log('taskIds', taskIds);
};
// this method loops over the load cue and performs the individual load method per asset
function loadQueueLoop(index, onTaskQueueDone) {
var task, params, scope,
i, j, callback, taskIds,
performCallback;
if (index === taskQueue.length) {
// call all callbacks that have to be called at the end of the loop queue
for (i = callbacks.length - 1; i >= 0; i--) {
callback = callbacks[i];
if (callback === false) {
// this callback has already been called
continue;
}
//console.log(i, callback.method);
var m = callback.method;
//callback = false;
//console.log(1,callback);
setTimeout(function () {
//console.log(2, m);
//callback.method();
m();
}, 0);
}
finishedTasks = {};
taskQueue = [];
callbacks = [];
taskIndex = 0;
busy = false;
if (onTaskQueueDone) {
// for internal use only, never used so far
console.log('onTaskQueueDone');
onTaskQueueDone();
}
//console.log('task queue done', sequencer.storage);
return;
}
task = taskQueue[index];
scope = task.scope || null;
params = task.params || [];
//console.log(index, task.type, taskQueue.length);
if (typeString(params) !== 'array') {
params = [params];
}
function cbActionLoop(success) {
//console.log('cbActionLoop', success);
// set a flag that this task has been done
finishedTasks[task.id] = true;
// check which callbacks we can call now
for (i = callbacks.length - 1; i >= 0; i--) {
callback = callbacks[i];
if (callback === false) {
// this callback has already been called
continue;
}
taskIds = callback.taskIds;
// console.log(i, callback.method, taskIds);
// some callbacks may only be called after a task, or a number of tasks have been done
if (taskIds !== undefined) {
performCallback = true;
for (j = taskIds.length - 1; j >= 0; j--) {
// if one of the required tasks has not been done yet, do not perform the callback
if (finishedTasks[taskIds[j]] !== true) {
performCallback = false;
}
}
//console.log('performCallback', performCallback);
if (performCallback) {
//callback.method.call(null);
//console.log(callback);
var m = callback.method;
callbacks[i] = false;
setTimeout(function () {
m(success);
//console.log(callbacks);
}, 0);
}
}
}
//console.log('task done', task.name, index, taskQueue.length);
index++;
// if(index === taskQueue.length && taskIds === undefined){
// }
loadQueueLoop(index, onTaskQueueDone);
}
params.push(cbActionLoop);
//console.log(index, taskQueue.length, task.method.name, params);
task.method.apply(scope, params);
}
sequencer.getInstrument = function (path, exact_match) {
return findItem(path, storage.instruments, exact_match);
};
sequencer.getMidiFile = function (path, exact_match) {
return findItem(path, storage.midi, exact_match);
};
sequencer.getSamplePack = function (path, exact_match) {
return findItem(path, storage.samplepacks, exact_match);
};
sequencer.getSample = function (path, exact_match) {
return findItem(path, storage.audio, exact_match);
};
sequencer.getAssetPack = function (path, exact_match) {
return findItem(path, storage.assetpacks, exact_match);
};
sequencer.getSamplePacks = function (path, include_subfolders) {
return findItemsInFolder(path, storage.samplepacks, include_subfolders);
};
sequencer.getAssetPacks = function (path, include_subfolders) {
return findItemsInFolder(path, storage.assetpacks, include_subfolders);
};
sequencer.getSamples = function (path, include_subfolders) {
return findItemsInFolder(path, storage.audio, include_subfolders);
};
sequencer.getInstruments = function (path, include_subfolders) {
return findItemsInFolder(path, storage.instruments, include_subfolders);
};
sequencer.getMidiFiles = function (path, include_subfolders) {
return findItemsInFolder(path, storage.midi, include_subfolders);
};
sequencer.protectedScope.addInitMethod(function () {
storage = sequencer.storage;
loadLoop = sequencer.protectedScope.loadLoop;
findItem = sequencer.protectedScope.findItem;
storeItem = sequencer.protectedScope.storeItem;
deleteItem = sequencer.protectedScope.deleteItem;
typeString = sequencer.protectedScope.typeString;
getArguments = sequencer.protectedScope.getArguments;
isEmptyObject = sequencer.protectedScope.isEmptyObject;
objectForEach = sequencer.protectedScope.objectForEach;
updateInstruments = sequencer.protectedScope.updateInstruments;
findItemsInFolder = sequencer.protectedScope.findItemsInFolder;
});
}
function assetPack() {
'use strict';
// console.log('AssetPack');
var
index = 0,
storage, // defined in open_module.js
ajax, // defined in utils.js
round, // defined in utils.js
parseUrl, // defined in utils.js
findItem, // defined in utils.js
storeItem, // defined in utils.js
deleteItem, // defined in utils.js
typeString, // defined in utils.js
objectForEach, // defined in utils.js
removeMidiFile, // defined in asset_manager.js
removeAssetPack, // defined in asset_manager.js
removeInstrument, // defined in asset_manager.js
removeSamplePack, // defined in asset_manager.js
AssetPack;
AssetPack = function (config) {
this.id = 'AP' + index++ + new Date().getTime();
this.name = this.id;
this.className = 'AssetPack';
this.loaded = false;
this.midifiles = config.midifiles || [];
this.samplepacks = config.samplepacks || [];
this.instruments = config.instruments || [];
this.url = config.url;
var pack = this;
objectForEach(config, function (val, key) {
pack[key] = val;
});
};
function cleanup(assetpack, callback) {
assetpack = null;
//console.log(callback.name);
callback(false);
}
function store(assetpack) {
var occupied = findItem(assetpack.localPath, sequencer.storage.assetpacks, true),
action = assetpack.action;
//console.log('occ', occupied);
if (occupied && occupied.className === 'AssetPack' && action !== 'overwrite') {
if (sequencer.debug >= 2) {
console.warn('there is already an AssetPack at', assetpack.localPath);
}
return true;
} else {
storeItem(assetpack, assetpack.localPath, sequencer.storage.assetpacks);
return false;
}
}
function load(pack, callback) {
if (pack.url !== undefined) {
ajax({
url: pack.url,
responseType: 'json',
onError: function (e) {
//console.log('onError', e);
cleanup(pack, callback);
},
onSuccess: function (data, fileSize) {
// if the json data is corrupt (for instance because of a trailing comma) data will be null
if (data === null) {
callback(false);
return;
}
pack.loaded = true;
if (data.name !== undefined && pack.name === undefined) {
pack.name = data.name;
}
if (data.folder !== undefined && pack.folder === undefined) {
pack.folder = data.folder;
}
if (pack.name === undefined) {
pack.name = parseUrl(pack.url).name;
}
pack.localPath = pack.folder !== undefined ? pack.folder + '/' + pack.name : pack.name;
pack.filesize = fileSize;
//pack.fileSize = round(data.length/1024/1024, 2);
//console.log(pack.filesize);
if (data.instruments) {
pack.instruments = pack.instruments.concat(data.instruments);
}
if (data.samplepacks) {
pack.samplepacks = pack.samplepacks.concat(data.samplepacks);
}
if (data.midifiles) {
pack.midifiles = pack.midifiles.concat(data.midifiles);
}
loadLoop(pack, callback);
}
});
} else {
pack.localPath = pack.folder !== undefined ? pack.folder + '/' + pack.name : pack.name;
loadLoop(pack, callback);
}
}
function loadLoop(assetpack, callback) {
var i, assets, asset,
loaded = store(assetpack),
localPath = assetpack.localPath;
if (loaded === true) {
assetpack = findItem(localPath, sequencer.storage.assetpacks, true);
callback(assetpack);
return;
}
if (assetpack.url !== undefined) {
var packs = sequencer.storage.assetpacks,
tmp, p, double = null;
for (p in packs) {
tmp = packs[p];
if (tmp.className !== 'AssetPack') {
continue;
}
//console.log('loop', p, assetpack.id);
if (tmp.id !== assetpack.id && tmp.url === assetpack.url) {
double = tmp;
break;
}
}
if (double !== null) {
//console.log(double.id, assetpack.id);
localPath = assetpack.localPath;
removeAssetPack(localPath);
assetpack = null;
assetpack = findItem(double.localPath, sequencer.storage.assetpacks, true);
//console.log(assetpack.id, double.id);
callback(assetpack);
return;
}
}
assets = assetpack.midifiles;
for (i = assets.length - 1; i >= 0; i--) {
//console.log('midifile', assets[i]);
asset = assets[i];
asset.pack = assetpack;
sequencer.addMidiFile(asset);
}
assets = assetpack.instruments;
for (i = assets.length - 1; i >= 0; i--) {
//console.log('instrument', assets[i]);
asset = assets[i];
asset.pack = assetpack;
sequencer.addInstrument(asset);
}
assets = assetpack.samplepacks;
for (i = assets.length - 1; i >= 0; i--) {
//console.log('samplepack', assets[i], pack);
asset = assets[i];
asset.pack = assetpack;
//console.log(asset.folder, pack.fileSize);
sequencer.addSamplePack(asset);
}
callback(assetpack);
}
AssetPack.prototype.unload = function () {
var i, assets, asset;
assets = this.midifiles;
for (i = assets.length - 1; i >= 0; i--) {
asset = assets[i];
removeMidiFile(asset.folder + '/' + asset.name);
}
assets = this.instruments;
for (i = assets.length - 1; i >= 0; i--) {
asset = assets[i];
removeInstrument(asset.folder + '/' + asset.name);
}
assets = this.samplepacks;
for (i = assets.length - 1; i >= 0; i--) {
asset = assets[i];
removeSamplePack(asset.folder + '/' + asset.name);
}
deleteItem(this.localPath, storage.assetpacks);
};
sequencer.addAssetPack = function (config, callback) {
var type = typeString(config),
assetpack, json, name, folder;
if (type !== 'object') {
if (sequencer.debug >= 2) {
console.warn('can\'t create an AssetPack with this data', config);
}
return false;
}
if (callback === undefined) {
callback = function () { };
}
if (config.json) {
json = config.json;
name = config.name;
folder = config.folder;
if (typeString(json) === 'string') {
try {
json = JSON.parse(json);
} catch (e) {
if (sequencer.debug >= 2) {
console.warn('can\'t create an AssetPack with this data', config);
}
return false;
}
}
if (json.instruments === undefined && json.midifiles === undefined && json.samplepacks === undefined) {
if (sequencer.debug >= 2) {
console.warn('can\'t create an AssetPack with this data', config);
}
return false;
}
config = {
midifiles: json.midifiles,
instruments: json.instruments,
samplepacks: json.samplepacks,
name: name === undefined ? json.name : name,
folder: folder === undefined ? json.folder : folder
};
//console.log('config', name, folder, json.name, json.folder);
}
//assetpack = new AssetPack(config);
//console.log(assetpack.id);
sequencer.addTask({
type: 'load asset pack',
method: load,
params: new AssetPack(config)
}, function (assetpack) {
config = null;
// console.log(assetpack.id);
callback(assetpack);
//console.log('assetpack', assetpack);
}, true);
sequencer.startTaskQueue();
/*
sequencer.addTask({
method: load,
params: assetpack
}, function(){
console.log('loaded', assetpack);
store(assetpack);
if(callback){
callback(assetpack);
}
});
*/
};
sequencer.protectedScope.addInitMethod(function () {
ajax = sequencer.protectedScope.ajax;
round = sequencer.protectedScope.round;
parseUrl = sequencer.protectedScope.parseUrl;
findItem = sequencer.protectedScope.findItem;
storeItem = sequencer.protectedScope.storeItem;
deleteItem = sequencer.protectedScope.deleteItem;
typeString = sequencer.protectedScope.typeString;
objectForEach = sequencer.protectedScope.objectForEach;
storage = sequencer.storage;
removeMidiFile = sequencer.removeMidiFile;
removeInstrument = sequencer.removeInstrument;
removeSamplePack = sequencer.removeSamplePack;
removeAssetPack = sequencer.removeAssetPack;
});
}
function audioEvent() {
'use strict';
var
slice = Array.prototype.slice,
//import
typeString, // → defined in utils.js
AudioEvent,
audioEventId = 0;
AudioEvent = function (config) {
if (config === undefined) {
// bypass for cloning
return;
}
// use ticks like in MidiEvent
if (config.ticks === undefined) {
this.ticks = 0;
} else {
this.ticks = config.ticks;
}
// provide either buffer (AudioBuffer) or path to a sample in the sequencer.storage object
this.buffer = config.buffer;
this.sampleId = config.sampleId;
this.path = config.path;
if (this.buffer === undefined && this.path === undefined) {
if (sequencer.debug >= sequencer.WARN) {
console.warn('please provide an AudioBuffer or a path to a sample in the sequencer.storage object');
}
return;
}
if (this.buffer !== undefined && typeString(this.buffer) !== 'audiobuffer') {
if (sequencer.debug >= sequencer.WARN) {
console.warn('buffer has to be an AudioBuffer');
}
return;
}
if (this.path !== undefined) {
if (typeString(this.path) !== 'string') {
if (sequencer.debug >= sequencer.WARN) {
console.warn('path has to be a String');
}
return;
} else {
this.sampleId = this.path;
this.sampleId = this.sampleId.replace(/^\//, '');
this.sampleId = this.sampleId.replace(/\/$/, '');
this.sampleId = this.sampleId.split('/');
this.sampleId = this.sampleId[this.sampleId.length - 1];
this.buffer = sequencer.getSample(this.path);
if (this.buffer === false) {
if (sequencer.debug >= sequencer.WARN) {
console.warn('no sample found at', this.path);
}
return;
}
this.buffer = sequencer.getSample(this.path);
//console.log(this.sampleId, this.path, this.buffer);
//console.log(this.buffer);
}
}
// set either durationTicks of durationMillis, or both if they represent the same value
this.durationTicks = config.durationTicks;
this.durationMillis = config.durationMillis;
//console.log(this.durationTicks, this.durationMillis);
if (this.durationTicks === undefined && this.durationMillis === undefined) {
this.duration = this.buffer.duration;
this.durationMillis = this.duration * 1000;
}
//console.log(this.durationMillis, this.duration, this.buffer);
this.muted = false;
if (config.velocity === undefined) {
this.velocity = 127;
} else {
this.velocity = config.velocity;
}
// start of audio, also the quantize point, value in ticks or millis
this.sampleOffsetTicks = config.sampleOffsetTicks;
this.sampleOffsetMillis = config.sampleOffsetMillis;
if (this.sampleOffsetMillis === undefined && this.sampleOffsetTicks === undefined) {
this.sampleOffsetTicks = 0;
this.sampleOffsetMillis = 0;
this.sampleOffset = 0;
} else if (this.sampleOffsetMillis !== undefined) {
this.sampleOffset = this.sampleOffsetMillis / 1000;
}
this.latencyCompensation = config.latencyCompensation;
if (this.latencyCompensation === undefined) {
this.latencyCompensation = 0;
}
// if the playhead starts somewhere in the sample, this value will be set by the scheduler
this.playheadOffset = 0;
this.className = 'AudioEvent';
this.time = 0;
this.type = 'audio';
this.id = 'A' + audioEventId + new Date().getTime();
};
AudioEvent.prototype.update = function () {
var pos;
if (this.duration === undefined) {
pos = this.song.getPosition('ticks', this.ticks + this.durationTicks);
this.durationMillis = pos.millis - this.millis;
this.duration = this.durationMillis / 1000;
//console.log(pos, this.durationMillis);
} else if (this.durationTicks === undefined) {
pos = this.song.getPosition('millis', this.millis + this.durationMillis);
this.durationTicks = pos.ticks - this.ticks;
}
if (this.sampleOffset === undefined) {
pos = this.song.getPosition('ticks', this.ticks + this.sampleOffsetTicks);
//console.log(pos.barsAsString);
this.sampleOffsetMillis = pos.millis - this.millis;
this.sampleOffset = this.sampleOffsetMillis / 1000;
//console.log(this.sampleOffsetMillis);
} else if (this.sampleOffsetTicks === undefined) {
pos = this.song.getPosition('millis', this.millis + this.sampleOffsetMillis);
this.sampleOffsetTicks = pos.ticks - this.ticks;
}
this.endTicks = this.ticks + this.durationTicks;
this.endMillis = this.millis + this.durationMillis;
};
AudioEvent.prototype.stopSample = function (seconds) {
this.track.audio.stopSample(this, seconds);
};
AudioEvent.prototype.setSampleOffset = function (type, value) {
if (type === 'millis') {
this.sampleOffsetMillis = value;
this.sampleOffset = value / 1000;
this.durationTicks = undefined;
if (this.song !== undefined) {
this.update();
}
} else if (type === 'ticks') {
this.sampleOffsetTicks = value;
this.sampleOffset = undefined;
this.sampleOffsetMillis = undefined;
if (this.song !== undefined) {
this.update();
}
} else {
if (sequencer.debug >= sequencer.WARN) {
console.warn('you have to provide a type "ticks" or "millis" and a value');
}
}
};
AudioEvent.prototype.setDuration = function (type, value) {
if (type === 'millis') {
this.durationMillis = value;
this.duration = value / 1000;
this.durationTicks = undefined;
if (this.song !== undefined) {
this.update();
}
} else if (type === 'ticks') {
this.durationTicks = value;
this.duration = undefined;
this.durationMillis = undefined;
if (this.song !== undefined) {
this.update();
}
} else {
if (sequencer.debug >= sequencer.WARN) {
console.warn('you have to provide a type "ticks" or "millis" and a value');
}
}
};
AudioEvent.prototype.clone = AudioEvent.prototype.copy = function () {
var event = new AudioEvent(),
property;
for (property in this) {
if (this.hasOwnProperty(property)) {
//console.log(property);
if (property !== 'id' && property !== 'eventNumber') {
event[property] = this[property];
}
event.song = undefined;
event.track = undefined;
event.trackId = undefined;
event.part = undefined;
event.partId = undefined;
}
}
return event;
};
// same as MidiEvent, could be inherited from generic Event
AudioEvent.prototype.reset = function (fromPart, fromTrack, fromSong) {
fromPart = fromPart === undefined ? true : false;
fromTrack = fromTrack === undefined ? true : false;
fromSong = fromSong === undefined ? true : false;
if (fromPart) {
this.part = undefined;
this.partId = undefined;
}
if (fromTrack) {
this.track = undefined;
this.trackId = undefined;
this.channel = 0;
}
if (fromSong) {
this.song = undefined;
}
};
// same as MidiEvent, could be inherited from generic Event
AudioEvent.prototype.move = function (ticks) {
if (isNaN(ticks)) {
if (sequencer.debug >= 1) {
console.error('please provide a number');
}
return;
}
this.ticks += parseInt(ticks, 10);
if (this.song !== undefined) {
this.update();
}
if (this.state !== 'new') {
this.state = 'changed';
}
if (this.part !== undefined) {
this.part.needsUpdate = true;
}
};
// same as MidiEvent, could be inherited from generic Event
AudioEvent.prototype.moveTo = function () {
var position = slice.call(arguments);
//console.log(position);
if (position[0] === 'ticks' && isNaN(position[1]) === false) {
this.ticks = parseInt(position[1], 10);
} else if (this.song === undefined) {
if (sequencer.debug >= 1) {
console.error('The audio event has not been added to a song yet; you can only move to ticks values');
}
} else {
position = this.song.getPosition(position);
if (position === false) {
if (sequencer.debug >= 1) {
console.error('wrong position data');
}
} else {
this.ticks = position.ticks;
}
}
if (this.song !== undefined) {
this.update();
}
if (this.state !== 'new') {
this.state = 'changed';
}
if (this.part !== undefined) {
this.part.needsUpdate = true;
}
};
sequencer.createAudioEvent = function (config) {
if (config.className === 'AudioEvent') {
return config.clone();
}
return new AudioEvent(config);
};
sequencer.protectedScope.addInitMethod(function () {
typeString = sequencer.protectedScope.typeString;
});
}function audioRecorder() {
'use strict';
var
// import
context, // defined in open_module.js
encode64, // defined in util.js
dispatchEvent, // defined in song_event_listener.js
createWorker, // defined in audio_recorder_worker.js
getWaveformData, //defined in util.js
microphoneAccessGranted = null,
localMediaStream,
bufferSize = 8192,
millisPerSample,
bufferMillis,
waveformConfig = {
height: 200,
width: 800,
//density: 0.0001,
sampleStep: 1,
color: '#71DE71',
bgcolor: '#000'
};
function AudioRecorder(track) {
this.track = track;
this.song = track.song;
this.audioEvents = {};
this.callback = null; // callback after wav audio file of the recording has been created or updated
this.worker = createWorker();
this.waveformConfig = track.waveformConfig || waveformConfig;
var scope = this;
this.worker.onmessage = function (e) {
//createAudioBuffer(scope, e.data.wavArrayBuffer, e.data.interleavedSamples, e.data.planarSamples, e.data.id);
encodeAudioBuffer(scope, e.data.wavArrayBuffer, e.data.interleavedSamples, e.data.id);
};
}
function createAudioBuffer(scope, wavArrayBuffer, interleavedSamples, planarSamples, type) {
var
i,
frameCount = planarSamples.length,
base64 = encode64(wavArrayBuffer),
audioBuffer = context.createBuffer(1, frameCount, context.sampleRate),
samples = audioBuffer.getChannelData(0),
recording = {
id: scope.recordId,
audioBuffer: null,
wavArrayBuffer: wavArrayBuffer,
wav: {
blob: new Blob([new Uint8Array(wavArrayBuffer)], { type: 'audio/wav' }),
base64: base64,
dataUrl: 'data:audio/wav;base64,' + base64
},
waveform: {}
};
for (let i = 0; i < frameCount; i++) {
samples[i] = planarSamples[i];
}
recording.audioBuffer = audioBuffer;
// keep a copy of the original samples for non-destructive editing
if (type === 'new') {
recording.planarSamples = planarSamples;
recording.interleavedSamples = interleavedSamples;
} else {
recording.planarSamples = sequencer.storage.audio.recordings[scope.recordId].planarSamples;
recording.interleavedSamples = sequencer.storage.audio.recordings[scope.recordId].interleavedSamples;
}
sequencer.storage.audio.recordings[scope.recordId] = recording;
//console.log('create took', window.performance.now() - scope.timestamp);
if (scope.callback !== null) {
scope.callback(recording);
scope.callback = null;
}
}
function encodeAudioBuffer(scope, wavArrayBuffer, interleavedSamples, type) {
//console.log(wavArrayBuffer, interleavedSamples, type);
context.decodeAudioData(wavArrayBuffer, function (audioBuffer) {
var
base64 = encode64(wavArrayBuffer),
recording = {
id: scope.recordId,
audioBuffer: audioBuffer,
wavArrayBuffer: wavArrayBuffer,
wav: {
blob: new Blob([new Uint8Array(wavArrayBuffer)], { type: 'audio/wav' }),
base64: base64,
dataUrl: 'data:audio/wav;base64,' + base64
},
waveform: {}
};
// keep a copy of the original samples for non-destructive editing
if (type === 'new') {
recording.interleavedSamples = interleavedSamples;
} else {
recording.interleavedSamples = sequencer.storage.audio.recordings[scope.recordId].interleavedSamples;
}
// create waveform images
getWaveformData(
audioBuffer,
scope.waveformConfig,
//callback
function (data) {
recording.waveform = { dataUrls: data };
sequencer.storage.audio.recordings[scope.recordId] = recording;
//console.log('encode took', window.performance.now() - scope.timestamp);
if (scope.callback !== null) {
scope.callback(recording);
scope.callback = null;
}
}
);
}, function () {
if (sequencer.debug >= sequencer.WARN) {
console.warn('no valid audiodata');
}
});
}
function record(callback) {
navigator.getUserMedia({ audio: true },
// successCallback
function (stream) {
microphoneAccessGranted = true;
// localMediaStream is type of MediaStream that comes from microphone
localMediaStream = stream;
//console.log(localMediaStream.getAudioTracks());
//console.log(localMediaStream.getVideoTracks());
callback();
},
// errorCallback
function (error) {
if (sequencer.debug >= sequencer.WARN) {
console.log(error);
}
microphoneAccessGranted = false;
callback();
}
);
}
// this triggers the little popup in the browser where the user has to grant access to her microphone
AudioRecorder.prototype.prepare = function (recordId, callback) {
var scope = this;
this.recordId = recordId;
if (microphoneAccessGranted === null) {
record(function () {
callback(microphoneAccessGranted);
if (localMediaStream !== undefined) {
//scope.localMediaStream = localMediaStream.clone(); -> not implemented yet
scope.start();
}
});
} else {
callback(microphoneAccessGranted);
if (localMediaStream !== undefined) {
//this.localMediaStream = localMediaStream.clone(); -> not implemented yet
this.start();
}
}
};
AudioRecorder.prototype.start = function () {
var scope = this,
song = this.track.song;
scope.worker.postMessage({
command: 'init',
sampleRate: context.sampleRate
});
this.scriptProcessor = context.createScriptProcessor(bufferSize, 1, 1);
this.scriptProcessor.onaudioprocess = function (e) {
if (e.inputBuffer.numberOfChannels === 1) {
scope.worker.postMessage({
command: 'record_mono',
buffer: e.inputBuffer.getChannelData(0)
});
} else {
scope.worker.postMessage({
command: 'record_stereo',
buffer: [
e.inputBuffer.getChannelData(0),
e.inputBuffer.getChannelData(1)
]
});
}
if (song.recording === false && song.precounting === false) {
scope.createAudio();
}
};
this.sourceNode = context.createMediaStreamSource(localMediaStream);
this.sourceNode.connect(this.scriptProcessor);
this.scriptProcessor.connect(context.destination);
};
AudioRecorder.prototype.stop = function (callback) {
this.stopRecordingTimestamp = context.currentTime * 1000;
this.timestamp = window.performance.now();
if (this.sourceNode === undefined) {
callback();
return;
}
this.callback = callback;
};
// create wav audio file after recording has stopped
AudioRecorder.prototype.createAudio = function () {
this.sourceNode.disconnect(this.scriptProcessor);
this.scriptProcessor.disconnect(context.destination);
this.scriptProcessor.onaudioprocess = null;
this.sourceNode = null;
this.scriptProcessors = null;
// remove precount bars and latency
var bufferIndexStart = parseInt((this.song.metronome.precountDurationInMillis + this.song.audioRecordingLatency) / millisPerSample),
bufferIndexEnd = -1;
this.worker.postMessage({
command: 'get_wavfile',
//command: 'get_wavfile2', // use this if you want to create the audio buffer instead of decoding it
bufferIndexStart: bufferIndexStart,
bufferIndexEnd: bufferIndexEnd
});
};
// adjust latency for specific recording -> all audio events that use this audio data will be updated!
// if you don't want that, please use AudioEvent.sampleOffset to adjust the starting point of the audio data
AudioRecorder.prototype.setAudioRecordingLatency = function (recordId, value, callback) {
var bufferIndexStart = parseInt(value / millisPerSample),
bufferIndexEnd = -1;
this.callback = callback;
this.worker.postMessage({
command: 'update_wavfile',
samples: sequencer.storage.audio.recordings[recordId].interleavedSamples,
bufferIndexStart: bufferIndexStart,
bufferIndexEnd: bufferIndexEnd
});
};
AudioRecorder.prototype.cleanup = function () {
if (localMediaStream === undefined) {
this.worker.terminate();
return;
}
//this.localMediaStream.stop();
this.scriptProcessor.disconnect();
this.scriptProcessor.onaudioprocess = null;
this.sourceNode.disconnect();
this.scriptProcessor = null;
this.sourceNode = null;
this.worker.terminate();
};
sequencer.protectedScope.createAudioRecorder = function (track) {
if (sequencer.record_audio === false) {
return false;
}
return new AudioRecorder(track);
};
sequencer.protectedScope.addInitMethod(function () {
encode64 = sequencer.util.encode64;
context = sequencer.protectedScope.context;
getWaveformData = sequencer.getWaveformData;
createWorker = sequencer.protectedScope.createAudioR