|
@@ -0,0 +1,212 @@
|
|
|
+/* 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
|