UNPKG

soundfont-player

Version:

Lightweight soundfont (music instrument) loader and player for WebAudio API

1,177 lines (1,038 loc) 41 kB
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ 'use strict' var load = require('audio-loader') var player = require('sample-player') /** * Load a soundfont instrument. It returns a promise that resolves to a * instrument object. * * The instrument object returned by the promise has the following properties: * * - name: the instrument name * - play: A function to play notes from the buffer with the signature * `play(note, time, duration, options)` * * * The valid options are: * * - `format`: the soundfont format. 'mp3' by default. Can be 'ogg' * - `soundfont`: the soundfont name. 'MusyngKite' by default. Can be 'FluidR3_GM' * - `nameToUrl` <Function>: a function to convert from instrument names to URL * - `destination`: by default Soundfont uses the `audioContext.destination` but you can override it. * - `gain`: the gain of the player (1 by default) * - `notes`: an array of the notes to decode. It can be an array of strings * with note names or an array of numbers with midi note numbers. This is a * performance option: since decoding mp3 is a cpu intensive process, you can limit * limit the number of notes you want and reduce the time to load the instrument. * * @param {AudioContext} ac - the audio context * @param {String} name - the instrument name. For example: 'acoustic_grand_piano' * @param {Object} options - (Optional) the same options as Soundfont.loadBuffers * @return {Promise} * * @example * var Soundfont = require('sounfont-player') * Soundfont.instrument('marimba').then(function (marimba) { * marimba.play('C4') * }) */ function instrument (ac, name, options) { if (arguments.length === 1) return function (n, o) { return instrument(ac, n, o) } var opts = options || {} var isUrl = opts.isSoundfontURL || isSoundfontURL var toUrl = opts.nameToUrl || nameToUrl var url = isUrl(name) ? name : toUrl(name, opts.soundfont, opts.format) return load(ac, url, { only: opts.only || opts.notes }).then(function (buffers) { var p = player(ac, buffers, opts).connect(opts.destination ? opts.destination : ac.destination) p.url = url p.name = name return p }) } function isSoundfontURL (name) { return /\.js(\?.*)?$/i.test(name) } /** * Given an instrument name returns a URL to to the Benjamin Gleitzman's * package of [pre-rendered sound fonts](https://github.com/gleitz/midi-js-soundfonts) * * @param {String} name - instrument name * @param {String} soundfont - (Optional) the soundfont name. One of 'FluidR3_GM' * or 'MusyngKite' ('MusyngKite' by default) * @param {String} format - (Optional) Can be 'mp3' or 'ogg' (mp3 by default) * @returns {String} the Soundfont file url * @example * var Soundfont = require('soundfont-player') * Soundfont.nameToUrl('marimba', 'mp3') */ function nameToUrl (name, sf, format) { format = format === 'ogg' ? format : 'mp3' sf = sf === 'FluidR3_GM' ? sf : 'MusyngKite' return 'https://gleitz.github.io/midi-js-soundfonts/' + sf + '/' + name + '-' + format + '.js' } // In the 1.0.0 release it will be: // var Soundfont = {} var Soundfont = require('./legacy') Soundfont.instrument = instrument Soundfont.nameToUrl = nameToUrl if (typeof module === 'object' && module.exports) module.exports = Soundfont if (typeof window !== 'undefined') window.Soundfont = Soundfont },{"./legacy":2,"audio-loader":6,"sample-player":10}],2:[function(require,module,exports){ 'use strict' var parser = require('note-parser') /** * Create a Soundfont object * * @param {AudioContext} context - the [audio context](https://developer.mozilla.org/en/docs/Web/API/AudioContext) * @param {Function} nameToUrl - (Optional) a function that maps the sound font name to the url * @return {Soundfont} a soundfont object */ function Soundfont (ctx, nameToUrl) { console.warn('new Soundfont() is deprected') console.log('Please use Soundfont.instrument() instead of new Soundfont().instrument()') if (!(this instanceof Soundfont)) return new Soundfont(ctx) this.nameToUrl = nameToUrl || Soundfont.nameToUrl this.ctx = ctx this.instruments = {} this.promises = [] } Soundfont.prototype.onready = function (callback) { console.warn('deprecated API') console.log('Please use Promise.all(Soundfont.instrument(), Soundfont.instrument()).then() instead of new Soundfont().onready()') Promise.all(this.promises).then(callback) } Soundfont.prototype.instrument = function (name, options) { console.warn('new Soundfont().instrument() is deprecated.') console.log('Please use Soundfont.instrument() instead.') var ctx = this.ctx name = name || 'default' if (name in this.instruments) return this.instruments[name] var inst = {name: name, play: oscillatorPlayer(ctx, options)} this.instruments[name] = inst if (name !== 'default') { var promise = Soundfont.instrument(ctx, name, options).then(function (instrument) { inst.play = instrument.play return inst }) this.promises.push(promise) inst.onready = function (cb) { console.warn('onready is deprecated. Use Soundfont.instrument().then()') promise.then(cb) } } else { inst.onready = function (cb) { console.warn('onready is deprecated. Use Soundfont.instrument().then()') cb() } } return inst } /* * Load the buffers of a given instrument name. It returns a promise that resolves * to a hash with midi note numbers as keys, and audio buffers as values. * * @param {AudioContext} ac - the audio context * @param {String} name - the instrument name (it accepts an url if starts with "http") * @param {Object} options - (Optional) options object * @return {Promise} a promise that resolves to a Hash of { midiNoteNum: <AudioBuffer> } * * The options object accepts the following keys: * * - nameToUrl {Function}: a function to convert from instrument names to urls. * By default it uses Benjamin Gleitzman's package of * [pre-rendered sound fonts](https://github.com/gleitz/midi-js-soundfonts) * - notes {Array}: the list of note names to be decoded (all by default) * * @example * var Soundfont = require('soundfont-player') * Soundfont.loadBuffers(ctx, 'acoustic_grand_piano').then(function(buffers) { * buffers[60] // => An <AudioBuffer> corresponding to note C4 * }) */ function loadBuffers (ac, name, options) { console.warn('Soundfont.loadBuffers is deprecate.') console.log('Use Soundfont.instrument(..) and get buffers properties from the result.') return Soundfont.instrument(ac, name, options).then(function (inst) { return inst.buffers }) } Soundfont.loadBuffers = loadBuffers /** * Returns a function that plays an oscillator * * @param {AudioContext} ac - the audio context * @param {Hash} defaultOptions - (Optional) a hash of options: * - vcoType: the oscillator type (default: 'sine') * - gain: the output gain value (default: 0.4) * - destination: the player destination (default: ac.destination) */ function oscillatorPlayer (ctx, defaultOptions) { defaultOptions = defaultOptions || {} return function (note, time, duration, options) { console.warn('The oscillator player is deprecated.') console.log('Starting with version 0.9.0 you will have to wait until the soundfont is loaded to play sounds.') var midi = note > 0 && note < 129 ? +note : parser.midi(note) var freq = midi ? parser.midiToFreq(midi, 440) : null if (!freq) return duration = duration || 0.2 options = options || {} var destination = options.destination || defaultOptions.destination || ctx.destination var vcoType = options.vcoType || defaultOptions.vcoType || 'sine' var gain = options.gain || defaultOptions.gain || 0.4 var vco = ctx.createOscillator() vco.type = vcoType vco.frequency.value = freq /* VCA */ var vca = ctx.createGain() vca.gain.value = gain /* Connections */ vco.connect(vca) vca.connect(destination) vco.start(time) if (duration > 0) vco.stop(time + duration) return vco } } /** * Given a note name, return the note midi number * * @name noteToMidi * @function * @param {String} noteName * @return {Integer} the note midi number or null if not a valid note name */ Soundfont.noteToMidi = parser.midi module.exports = Soundfont },{"note-parser":8}],3:[function(require,module,exports){ module.exports = ADSR function ADSR(audioContext){ var node = audioContext.createGain() var voltage = node._voltage = getVoltage(audioContext) var value = scale(voltage) var startValue = scale(voltage) var endValue = scale(voltage) node._startAmount = scale(startValue) node._endAmount = scale(endValue) node._multiplier = scale(value) node._multiplier.connect(node) node._startAmount.connect(node) node._endAmount.connect(node) node.value = value.gain node.startValue = startValue.gain node.endValue = endValue.gain node.startValue.value = 0 node.endValue.value = 0 Object.defineProperties(node, props) return node } var props = { attack: { value: 0, writable: true }, decay: { value: 0, writable: true }, sustain: { value: 1, writable: true }, release: {value: 0, writable: true }, getReleaseDuration: { value: function(){ return this.release } }, start: { value: function(at){ var target = this._multiplier.gain var startAmount = this._startAmount.gain var endAmount = this._endAmount.gain this._voltage.start(at) this._decayFrom = this._decayFrom = at+this.attack this._startedAt = at var sustain = this.sustain target.cancelScheduledValues(at) startAmount.cancelScheduledValues(at) endAmount.cancelScheduledValues(at) endAmount.setValueAtTime(0, at) if (this.attack){ target.setValueAtTime(0, at) target.linearRampToValueAtTime(1, at + this.attack) startAmount.setValueAtTime(1, at) startAmount.linearRampToValueAtTime(0, at + this.attack) } else { target.setValueAtTime(1, at) startAmount.setValueAtTime(0, at) } if (this.decay){ target.setTargetAtTime(sustain, this._decayFrom, getTimeConstant(this.decay)) } } }, stop: { value: function(at, isTarget){ if (isTarget){ at = at - this.release } var endTime = at + this.release if (this.release){ var target = this._multiplier.gain var startAmount = this._startAmount.gain var endAmount = this._endAmount.gain target.cancelScheduledValues(at) startAmount.cancelScheduledValues(at) endAmount.cancelScheduledValues(at) var expFalloff = getTimeConstant(this.release) // truncate attack (required as linearRamp is removed by cancelScheduledValues) if (this.attack && at < this._decayFrom){ var valueAtTime = getValue(0, 1, this._startedAt, this._decayFrom, at) target.linearRampToValueAtTime(valueAtTime, at) startAmount.linearRampToValueAtTime(1-valueAtTime, at) startAmount.setTargetAtTime(0, at, expFalloff) } endAmount.setTargetAtTime(1, at, expFalloff) target.setTargetAtTime(0, at, expFalloff) } this._voltage.stop(endTime) return endTime } }, onended: { get: function(){ return this._voltage.onended }, set: function(value){ this._voltage.onended = value } } } var flat = new Float32Array([1,1]) function getVoltage(context){ var voltage = context.createBufferSource() var buffer = context.createBuffer(1, 2, context.sampleRate) buffer.getChannelData(0).set(flat) voltage.buffer = buffer voltage.loop = true return voltage } function scale(node){ var gain = node.context.createGain() node.connect(gain) return gain } function getTimeConstant(time){ return Math.log(time+1)/Math.log(100) } function getValue(start, end, fromTime, toTime, at){ var difference = end - start var time = toTime - fromTime var truncateTime = at - fromTime var phase = truncateTime / time var value = start + phase * difference if (value <= start) { value = start } if (value >= end) { value = end } return value } },{}],4:[function(require,module,exports){ 'use strict' // DECODE UTILITIES function b64ToUint6 (nChr) { return nChr > 64 && nChr < 91 ? nChr - 65 : nChr > 96 && nChr < 123 ? nChr - 71 : nChr > 47 && nChr < 58 ? nChr + 4 : nChr === 43 ? 62 : nChr === 47 ? 63 : 0 } // Decode Base64 to Uint8Array // --------------------------- function decode (sBase64, nBlocksSize) { var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, '') var nInLen = sB64Enc.length var nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2 var taBytes = new Uint8Array(nOutLen) for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { nMod4 = nInIdx & 3 nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4 if (nMod4 === 3 || nInLen - nInIdx === 1) { for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255 } nUint24 = 0 } } return taBytes } module.exports = { decode: decode } },{}],5:[function(require,module,exports){ /* global XMLHttpRequest */ 'use strict' /** * Given a url and a return type, returns a promise to the content of the url * Basically it wraps a XMLHttpRequest into a Promise * * @param {String} url * @param {String} type - can be 'text' or 'arraybuffer' * @return {Promise} */ module.exports = function (url, type) { return new Promise(function (done, reject) { var req = new XMLHttpRequest() if (type) req.responseType = type req.open('GET', url) req.onload = function () { req.status === 200 ? done(req.response) : reject(Error(req.statusText)) } req.onerror = function () { reject(Error('Network Error')) } req.send() }) } },{}],6:[function(require,module,exports){ 'use strict' var base64 = require('./base64') var fetch = require('./fetch') // Given a regex, return a function that test if against a string function fromRegex (r) { return function (o) { return typeof o === 'string' && r.test(o) } } // Try to apply a prefix to a name function prefix (pre, name) { return typeof pre === 'string' ? pre + name : typeof pre === 'function' ? pre(name) : name } /** * Load one or more audio files * * * Possible option keys: * * - __from__ {Function|String}: a function or string to convert from file names to urls. * If is a string it will be prefixed to the name: * `load(ac, 'snare.mp3', { from: 'http://audio.net/samples/' })` * If it's a function it receives the file name and should return the url as string. * - __only__ {Array} - when loading objects, if provided, only the given keys * will be included in the decoded object: * `load(ac, 'piano.json', { only: ['C2', 'D2'] })` * * @param {AudioContext} ac - the audio context * @param {Object} source - the object to be loaded * @param {Object} options - (Optional) the load options for that object * @param {Object} defaultValue - (Optional) the default value to return as * in a promise if not valid loader found */ function load (ac, source, options, defVal) { var loader = // Basic audio loading isArrayBuffer(source) ? loadArrayBuffer : isAudioFileName(source) ? loadAudioFile : isPromise(source) ? loadPromise // Compound objects : isArray(source) ? loadArrayData : isObject(source) ? loadObjectData : isJsonFileName(source) ? loadJsonFile // Base64 encoded audio : isBase64Audio(source) ? loadBase64Audio : isJsFileName(source) ? loadMidiJSFile : null var opts = options || {} return loader ? loader(ac, source, opts) : defVal ? Promise.resolve(defVal) : Promise.reject('Source not valid (' + source + ')') } load.fetch = fetch // BASIC AUDIO LOADING // =================== // Load (decode) an array buffer function isArrayBuffer (o) { return o instanceof ArrayBuffer } function loadArrayBuffer (ac, array, options) { return new Promise(function (done, reject) { ac.decodeAudioData(array, function (buffer) { done(buffer) }, function () { reject("Can't decode audio data (" + array.slice(0, 30) + '...)') } ) }) } // Load an audio filename var isAudioFileName = fromRegex(/\.(mp3|wav|ogg)(\?.*)?$/i) function loadAudioFile (ac, name, options) { var url = prefix(options.from, name) return load(ac, load.fetch(url, 'arraybuffer'), options) } // Load the result of a promise function isPromise (o) { return o && typeof o.then === 'function' } function loadPromise (ac, promise, options) { return promise.then(function (value) { return load(ac, value, options) }) } // COMPOUND OBJECTS // ================ // Try to load all the items of an array var isArray = Array.isArray function loadArrayData (ac, array, options) { return Promise.all(array.map(function (data) { return load(ac, data, options, data) })) } // Try to load all the values of a key/value object function isObject (o) { return o && typeof o === 'object' } function loadObjectData (ac, obj, options) { var dest = {} var promises = Object.keys(obj).map(function (key) { if (options.only && options.only.indexOf(key) === -1) return null var value = obj[key] return load(ac, value, options, value).then(function (audio) { dest[key] = audio }) }) return Promise.all(promises).then(function () { return dest }) } // Load the content of a JSON file var isJsonFileName = fromRegex(/\.json(\?.*)?$/i) function loadJsonFile (ac, name, options) { var url = prefix(options.from, name) return load(ac, load.fetch(url, 'text').then(JSON.parse), options) } // BASE64 ENCODED FORMATS // ====================== // Load strings with Base64 encoded audio var isBase64Audio = fromRegex(/^data:audio/) function loadBase64Audio (ac, source, options) { var i = source.indexOf(',') return load(ac, base64.decode(source.slice(i + 1)).buffer, options) } // Load .js files with MidiJS soundfont prerendered audio var isJsFileName = fromRegex(/\.js(\?.*)?$/i) function loadMidiJSFile (ac, name, options) { var url = prefix(options.from, name) return load(ac, load.fetch(url, 'text').then(midiJsToJson), options) } // convert a MIDI.js javascript soundfont file to json function midiJsToJson (data) { var begin = data.indexOf('MIDI.Soundfont.') if (begin < 0) throw Error('Invalid MIDI.js Soundfont format') begin = data.indexOf('=', begin) + 2 var end = data.lastIndexOf(',') return JSON.parse(data.slice(begin, end) + '}') } if (typeof module === 'object' && module.exports) module.exports = load if (typeof window !== 'undefined') window.loadAudio = load },{"./base64":4,"./fetch":5}],7:[function(require,module,exports){ (function (global){ (function(e){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=e()}else if(typeof define==="function"&&define.amd){define([],e)}else{var t;if(typeof window!=="undefined"){t=window}else if(typeof global!=="undefined"){t=global}else if(typeof self!=="undefined"){t=self}else{t=this}t.midimessage=e()}})(function(){var e,t,s;return function o(e,t,s){function a(n,i){if(!t[n]){if(!e[n]){var l=typeof require=="function"&&require;if(!i&&l)return l(n,!0);if(r)return r(n,!0);var h=new Error("Cannot find module '"+n+"'");throw h.code="MODULE_NOT_FOUND",h}var c=t[n]={exports:{}};e[n][0].call(c.exports,function(t){var s=e[n][1][t];return a(s?s:t)},c,c.exports,o,e,t,s)}return t[n].exports}var r=typeof require=="function"&&require;for(var n=0;n<s.length;n++)a(s[n]);return a}({1:[function(e,t,s){"use strict";Object.defineProperty(s,"__esModule",{value:true});s["default"]=function(e){function t(e){this._event=e;this._data=e.data;this.receivedTime=e.receivedTime;if(this._data&&this._data.length<2){console.warn("Illegal MIDI message of length",this._data.length);return}this._messageCode=e.data[0]&240;this.channel=e.data[0]&15;switch(this._messageCode){case 128:this.messageType="noteoff";this.key=e.data[1]&127;this.velocity=e.data[2]&127;break;case 144:this.messageType="noteon";this.key=e.data[1]&127;this.velocity=e.data[2]&127;break;case 160:this.messageType="keypressure";this.key=e.data[1]&127;this.pressure=e.data[2]&127;break;case 176:this.messageType="controlchange";this.controllerNumber=e.data[1]&127;this.controllerValue=e.data[2]&127;if(this.controllerNumber===120&&this.controllerValue===0){this.channelModeMessage="allsoundoff"}else if(this.controllerNumber===121){this.channelModeMessage="resetallcontrollers"}else if(this.controllerNumber===122){if(this.controllerValue===0){this.channelModeMessage="localcontroloff"}else{this.channelModeMessage="localcontrolon"}}else if(this.controllerNumber===123&&this.controllerValue===0){this.channelModeMessage="allnotesoff"}else if(this.controllerNumber===124&&this.controllerValue===0){this.channelModeMessage="omnimodeoff"}else if(this.controllerNumber===125&&this.controllerValue===0){this.channelModeMessage="omnimodeon"}else if(this.controllerNumber===126){this.channelModeMessage="monomodeon"}else if(this.controllerNumber===127){this.channelModeMessage="polymodeon"}break;case 192:this.messageType="programchange";this.program=e.data[1];break;case 208:this.messageType="channelpressure";this.pressure=e.data[1]&127;break;case 224:this.messageType="pitchbendchange";var t=e.data[2]&127;var s=e.data[1]&127;this.pitchBend=(t<<8)+s;break}}return new t(e)};t.exports=s["default"]},{}]},{},[1])(1)}); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}],8:[function(require,module,exports){ !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.NoteParser=t.NoteParser||{})}(this,function(t){"use strict";function n(t,n){return Array(n+1).join(t)}function r(t){return"number"==typeof t}function e(t){return"string"==typeof t}function u(t){return void 0!==t}function c(t,n){return Math.pow(2,(t-69)/12)*(n||440)}function o(){return b}function i(t,n,r){if("string"!=typeof t)return null;var e=b.exec(t);if(!e||!n&&e[4])return null;var u={letter:e[1].toUpperCase(),acc:e[2].replace(/x/g,"##")};u.pc=u.letter+u.acc,u.step=(u.letter.charCodeAt(0)+3)%7,u.alt="b"===u.acc[0]?-u.acc.length:u.acc.length;var o=A[u.step]+u.alt;return u.chroma=o<0?12+o:o%12,e[3]&&(u.oct=+e[3],u.midi=o+12*(u.oct+1),u.freq=c(u.midi,r)),n&&(u.tonicOf=e[4]),u}function f(t){return r(t)?t<0?n("b",-t):n("#",t):""}function a(t){return r(t)?""+t:""}function l(t,n,r){return null===t||void 0===t?null:t.step?l(t.step,t.alt,t.oct):t<0||t>6?null:C.charAt(t)+f(n)+a(r)}function p(t){if((r(t)||e(t))&&t>=0&&t<128)return+t;var n=i(t);return n&&u(n.midi)?n.midi:null}function s(t,n){var r=p(t);return null===r?null:c(r,n)}function d(t){return(i(t)||{}).letter}function m(t){return(i(t)||{}).acc}function h(t){return(i(t)||{}).pc}function v(t){return(i(t)||{}).step}function g(t){return(i(t)||{}).alt}function x(t){return(i(t)||{}).chroma}function y(t){return(i(t)||{}).oct}var b=/^([a-gA-G])(#{1,}|b{1,}|x{1,}|)(-?\d*)\s*(.*)\s*$/,A=[0,2,4,5,7,9,11],C="CDEFGAB";t.regex=o,t.parse=i,t.build=l,t.midi=p,t.freq=s,t.letter=d,t.acc=m,t.pc=h,t.step=v,t.alt=g,t.chroma=x,t.oct=y}); },{}],9:[function(require,module,exports){ module.exports = function (player) { /** * Adds a listener of an event * @chainable * @param {String} event - the event name * @param {Function} callback - the event handler * @return {SamplePlayer} the player * @example * player.on('start', function(time, note) { * console.log(time, note) * }) */ player.on = function (event, cb) { if (arguments.length === 1 && typeof event === 'function') return player.on('event', event) var prop = 'on' + event var old = player[prop] player[prop] = old ? chain(old, cb) : cb return player } return player } function chain (fn1, fn2) { return function (a, b, c, d) { fn1(a, b, c, d); fn2(a, b, c, d) } } },{}],10:[function(require,module,exports){ 'use strict' var player = require('./player') var events = require('./events') var notes = require('./notes') var scheduler = require('./scheduler') var midi = require('./midi') function SamplePlayer (ac, source, options) { return midi(scheduler(notes(events(player(ac, source, options))))) } if (typeof module === 'object' && module.exports) module.exports = SamplePlayer if (typeof window !== 'undefined') window.SamplePlayer = SamplePlayer },{"./events":9,"./midi":11,"./notes":12,"./player":13,"./scheduler":14}],11:[function(require,module,exports){ var midimessage = require('midimessage') module.exports = function (player) { /** * Connect a player to a midi input * * The options accepts: * * - channel: the channel to listen to. Listen to all channels by default. * * @param {MIDIInput} input * @param {Object} options - (Optional) * @return {SamplePlayer} the player * @example * var piano = player(...) * window.navigator.requestMIDIAccess().then(function (midiAccess) { * midiAccess.inputs.forEach(function (midiInput) { * piano.listenToMidi(midiInput) * }) * }) */ player.listenToMidi = function (input, options) { var started = {} var opts = options || {} var gain = opts.gain || function (vel) { return vel / 127 } input.onmidimessage = function (msg) { var mm = msg.messageType ? msg : midimessage(msg) if (mm.messageType === 'noteon' && mm.velocity === 0) { mm.messageType = 'noteoff' } if (opts.channel && mm.channel !== opts.channel) return switch (mm.messageType) { case 'noteon': started[mm.key] = player.play(mm.key, 0, { gain: gain(mm.velocity) }) break case 'noteoff': if (started[mm.key]) { started[mm.key].stop() delete started[mm.key] } break } } return player } return player } },{"midimessage":7}],12:[function(require,module,exports){ 'use strict' var note = require('note-parser') var isMidi = function (n) { return n !== null && n !== [] && n >= 0 && n < 129 } var toMidi = function (n) { return isMidi(n) ? +n : note.midi(n) } // Adds note name to midi conversion module.exports = function (player) { if (player.buffers) { var map = player.opts.map var toKey = typeof map === 'function' ? map : toMidi var mapper = function (name) { return name ? toKey(name) || name : null } player.buffers = mapBuffers(player.buffers, mapper) var start = player.start player.start = function (name, when, options) { var key = mapper(name) var dec = key % 1 if (dec) { key = Math.floor(key) options = Object.assign(options || {}, { cents: Math.floor(dec * 100) }) } return start(key, when, options) } } return player } function mapBuffers (buffers, toKey) { return Object.keys(buffers).reduce(function (mapped, name) { mapped[toKey(name)] = buffers[name] return mapped }, {}) } },{"note-parser":15}],13:[function(require,module,exports){ /* global AudioBuffer */ 'use strict' var ADSR = require('adsr') var EMPTY = {} var DEFAULTS = { gain: 1, attack: 0.01, decay: 0.1, sustain: 0.9, release: 0.3, loop: false, cents: 0, loopStart: 0, loopEnd: 0 } /** * Create a sample player. * * @param {AudioContext} ac - the audio context * @param {ArrayBuffer|Object<String,ArrayBuffer>} source * @param {Onject} options - (Optional) an options object * @return {player} the player * @example * var SamplePlayer = require('sample-player') * var ac = new AudioContext() * var snare = SamplePlayer(ac, <AudioBuffer>) * snare.play() */ function SamplePlayer (ac, source, options) { var connected = false var nextId = 0 var tracked = {} var out = ac.createGain() out.gain.value = 1 var opts = Object.assign({}, DEFAULTS, options) /** * @namespace */ var player = { context: ac, out: out, opts: opts } if (source instanceof AudioBuffer) player.buffer = source else player.buffers = source /** * Start a sample buffer. * * The returned object has a function `stop(when)` to stop the sound. * * @param {String} name - the name of the buffer. If the source of the * SamplePlayer is one sample buffer, this parameter is not required * @param {Float} when - (Optional) when to start (current time if by default) * @param {Object} options - additional sample playing options * @return {AudioNode} an audio node with a `stop` function * @example * var sample = player(ac, <AudioBuffer>).connect(ac.destination) * sample.start() * sample.start(5, { gain: 0.7 }) // name not required since is only one AudioBuffer * @example * var drums = player(ac, { snare: <AudioBuffer>, kick: <AudioBuffer>, ... }).connect(ac.destination) * drums.start('snare') * drums.start('snare', 0, { gain: 0.3 }) */ player.start = function (name, when, options) { // if only one buffer, reorder arguments if (player.buffer && name !== null) return player.start(null, name, when) var buffer = name ? player.buffers[name] : player.buffer if (!buffer) { console.warn('Buffer ' + name + ' not found.') return } else if (!connected) { console.warn('SamplePlayer not connected to any node.') return } var opts = options || EMPTY when = Math.max(ac.currentTime, when || 0) player.emit('start', when, name, opts) var node = createNode(name, buffer, opts) node.id = track(name, node) node.env.start(when) node.source.start(when) player.emit('started', when, node.id, node) if (opts.duration) node.stop(when + opts.duration) return node } // NOTE: start will be override so we can't copy the function reference // this is obviously not a good design, so this code will be gone soon. /** * An alias for `player.start` * @see player.start * @since 0.3.0 */ player.play = function (name, when, options) { return player.start(name, when, options) } /** * Stop some or all samples * * @param {Float} when - (Optional) an absolute time in seconds (or currentTime * if not specified) * @param {Array} nodes - (Optional) an array of nodes or nodes ids to stop * @return {Array} an array of ids of the stoped samples * * @example * var longSound = player(ac, <AudioBuffer>).connect(ac.destination) * longSound.start(ac.currentTime) * longSound.start(ac.currentTime + 1) * longSound.start(ac.currentTime + 2) * longSound.stop(ac.currentTime + 3) // stop the three sounds */ player.stop = function (when, ids) { var node ids = ids || Object.keys(tracked) return ids.map(function (id) { node = tracked[id] if (!node) return null node.stop(when) return node.id }) } /** * Connect the player to a destination node * * @param {AudioNode} destination - the destination node * @return {AudioPlayer} the player * @chainable * @example * var sample = player(ac, <AudioBuffer>).connect(ac.destination) */ player.connect = function (dest) { connected = true out.connect(dest) return player } player.emit = function (event, when, obj, opts) { if (player.onevent) player.onevent(event, when, obj, opts) var fn = player['on' + event] if (fn) fn(when, obj, opts) } return player // =============== PRIVATE FUNCTIONS ============== // function track (name, node) { node.id = nextId++ tracked[node.id] = node node.source.onended = function () { var now = ac.currentTime node.source.disconnect() node.env.disconnect() node.disconnect() player.emit('ended', now, node.id, node) } return node.id } function createNode (name, buffer, options) { var node = ac.createGain() node.gain.value = 0 // the envelope will control the gain node.connect(out) node.env = envelope(ac, options, opts) node.env.connect(node.gain) node.source = ac.createBufferSource() node.source.buffer = buffer node.source.connect(node) node.source.loop = options.loop || opts.loop node.source.playbackRate.value = centsToRate(options.cents || opts.cents) node.source.loopStart = options.loopStart || opts.loopStart node.source.loopEnd = options.loopEnd || opts.loopEnd node.stop = function (when) { var time = when || ac.currentTime player.emit('stop', time, name) var stopAt = node.env.stop(time) node.source.stop(stopAt) } return node } } function isNum (x) { return typeof x === 'number' } var PARAMS = ['attack', 'decay', 'sustain', 'release'] function envelope (ac, options, opts) { var env = ADSR(ac) var adsr = options.adsr || opts.adsr PARAMS.forEach(function (name, i) { if (adsr) env[name] = adsr[i] else env[name] = options[name] || opts[name] }) env.value.value = isNum(options.gain) ? options.gain : isNum(opts.gain) ? opts.gain : 1 return env } /* * Get playback rate for a given pitch change (in cents) * Basic [math](http://www.birdsoft.demon.co.uk/music/samplert.htm): * f2 = f1 * 2^( C / 1200 ) */ function centsToRate (cents) { return cents ? Math.pow(2, cents / 1200) : 1 } module.exports = SamplePlayer },{"adsr":3}],14:[function(require,module,exports){ 'use strict' var isArr = Array.isArray var isObj = function (o) { return o && typeof o === 'object' } var OPTS = {} module.exports = function (player) { /** * Schedule a list of events to be played at specific time. * * It supports three formats of events for the events list: * * - An array with [time, note] * - An array with [time, object] * - An object with { time: ?, [name|note|midi|key]: ? } * * @param {Float} time - an absolute time to start (or AudioContext's * currentTime if provided number is 0) * @param {Array} events - the events list. * @return {Array} an array of ids * * @example * // Event format: [time, note] * var piano = player(ac, ...).connect(ac.destination) * piano.schedule(0, [ [0, 'C2'], [0.5, 'C3'], [1, 'C4'] ]) * * @example * // Event format: an object { time: ?, name: ? } * var drums = player(ac, ...).connect(ac.destination) * drums.schedule(0, [ * { name: 'kick', time: 0 }, * { name: 'snare', time: 0.5 }, * { name: 'kick', time: 1 }, * { name: 'snare', time: 1.5 } * ]) */ player.schedule = function (time, events) { var now = player.context.currentTime var when = time < now ? now : time player.emit('schedule', when, events) var t, o, note, opts return events.map(function (event) { if (!event) return null else if (isArr(event)) { t = event[0]; o = event[1] } else { t = event.time; o = event } if (isObj(o)) { note = o.name || o.key || o.note || o.midi || null opts = o } else { note = o opts = OPTS } return player.start(note, when + (t || 0), opts) }) } return player } },{}],15:[function(require,module,exports){ 'use strict' var REGEX = /^([a-gA-G])(#{1,}|b{1,}|x{1,}|)(-?\d*)\s*(.*)\s*$/ /** * A regex for matching note strings in scientific notation. * * @name regex * @function * @return {RegExp} the regexp used to parse the note name * * The note string should have the form `letter[accidentals][octave][element]` * where: * * - letter: (Required) is a letter from A to G either upper or lower case * - accidentals: (Optional) can be one or more `b` (flats), `#` (sharps) or `x` (double sharps). * They can NOT be mixed. * - octave: (Optional) a positive or negative integer * - element: (Optional) additionally anything after the duration is considered to * be the element name (for example: 'C2 dorian') * * The executed regex contains (by array index): * * - 0: the complete string * - 1: the note letter * - 2: the optional accidentals * - 3: the optional octave * - 4: the rest of the string (trimmed) * * @example * var parser = require('note-parser') * parser.regex.exec('c#4') * // => ['c#4', 'c', '#', '4', ''] * parser.regex.exec('c#4 major') * // => ['c#4major', 'c', '#', '4', 'major'] * parser.regex().exec('CMaj7') * // => ['CMaj7', 'C', '', '', 'Maj7'] */ function regex () { return REGEX } var SEMITONES = [0, 2, 4, 5, 7, 9, 11] /** * Parse a note name in scientific notation an return it's components, * and some numeric properties including midi number and frequency. * * @name parse * @function * @param {String} note - the note string to be parsed * @param {Boolean} isTonic - true if the note is the tonic of something. * If true, en extra tonicOf property is returned. It's false by default. * @param {Float} tunning - The frequency of A4 note to calculate frequencies. * By default it 440. * @return {Object} the parsed note name or null if not a valid note * * The parsed note name object will ALWAYS contains: * - letter: the uppercase letter of the note * - acc: the accidentals of the note (only sharps or flats) * - pc: the pitch class (letter + acc) * - step: s a numeric representation of the letter. It's an integer from 0 to 6 * where 0 = C, 1 = D ... 6 = B * - alt: a numeric representation of the accidentals. 0 means no alteration, * positive numbers are for sharps and negative for flats * - chroma: a numeric representation of the pitch class. It's like midi for * pitch classes. 0 = C, 1 = C#, 2 = D ... It can have negative values: -1 = Cb. * Can detect pitch class enhramonics. * * If the note has octave, the parser object will contain: * - oct: the octave number (as integer) * - midi: the midi number * - freq: the frequency (using tuning parameter as base) * * If the parameter `isTonic` is set to true, the parsed object will contain: * - tonicOf: the rest of the string that follows note name (left and right trimmed) * * @example * var parse = require('note-parser').parse * parse('Cb4') * // => { letter: 'C', acc: 'b', pc: 'Cb', step: 0, alt: -1, chroma: -1, * oct: 4, midi: 59, freq: 246.94165062806206 } * // if no octave, no midi, no freq * parse('fx') * // => { letter: 'F', acc: '##', pc: 'F##', step: 3, alt: 2, chroma: 7 }) */ function parse (str, isTonic, tuning) { if (typeof str !== 'string') return null var m = REGEX.exec(str) if (!m || !isTonic && m[4]) return null var p = { letter: m[1].toUpperCase(), acc: m[2].replace(/x/g, '##') } p.pc = p.letter + p.acc p.step = (p.letter.charCodeAt(0) + 3) % 7 p.alt = p.acc[0] === 'b' ? -p.acc.length : p.acc.length p.chroma = SEMITONES[p.step] + p.alt if (m[3]) { p.oct = +m[3] p.midi = p.chroma + 12 * (p.oct + 1) p.freq = midiToFreq(p.midi, tuning) } if (isTonic) p.tonicOf = m[4] return p } /** * Given a midi number, return its frequency * @param {Integer} midi - midi note number * @param {Float} tuning - (Optional) the A4 tuning (440Hz by default) * @return {Float} frequency in hertzs */ function midiToFreq (midi, tuning) { return Math.pow(2, (midi - 69) / 12) * (tuning || 440) } var parser = { parse: parse, regex: regex, midiToFreq: midiToFreq } var FNS = ['letter', 'acc', 'pc', 'step', 'alt', 'chroma', 'oct', 'midi', 'freq'] FNS.forEach(function (name) { parser[name] = function (src) { var p = parse(src) return p && (typeof p[name] !== 'undefined') ? p[name] : null } }) module.exports = parser // extra API docs /** * Get midi of a note * * @name midi * @function * @param {String} note - the note name * @return {Integer} the midi number of the note or null if not a valid note * or the note does NOT contains octave * @example * var parser = require('note-parser') * parser.midi('A4') // => 69 * parser.midi('A') // => null */ /** * Get freq of a note in hertzs (in a well tempered 440Hz A4) * * @name freq * @function * @param {String} note - the note name * @return {Float} the freq of the number if hertzs or null if not valid note * or the note does NOT contains octave * @example * var parser = require('note-parser') * parser.freq('A4') // => 440 * parser.freq('A') // => null */ },{}]},{},[1]);