UNPKG

webdaw-modules

Version:

a set of modules for building a web-based DAW

1,583 lines (1,332 loc) 662 kB
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