danigb/timestretch

View on GitHub
examples/js/overlap-demo.js

Summary

Maintainability
F
3 wks
Test Coverage
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){

/* global AudioContext */
var ac = new AudioContext()
var load = require('audio-loader')
var play = require('./play')(ac)
var ts = require('../..')

var SEQ = 4410

console.log('Loading audio...')
load(ac, 'audio/amen.wav').then(function (buffer) {
  draw(document.getElementById('source'), buffer)
  draw(document.getElementById('sliced'),
    ts.stretch(ac, buffer, 0.9, { overlap: 1 }))
  draw(document.getElementById('overlap'),
    ts.stretch(ac, buffer, 0.9))
})

function draw (canvas, buffer) {
  canvas.onclick = function (e) {
    play(0, 4, buffer)
  }
  var x, y
  var data = buffer.getChannelData(0)
  var endSeq = Math.round(canvas.width / 5)
  var hm = Math.round(canvas.height / 2)
  var start = SEQ - endSeq

  var ctx = canvas.getContext('2d')
  ctx.fillStyle = '#efefef'
  ctx.fillRect(0, 0, canvas.width, canvas.height)

  ctx.strokeStyle = '#333'
  ctx.beginPath()
  for (x = 0; x < endSeq ; x++) {
    y = hm + Math.round(hm * data[x + start])
    ctx.lineTo(x, y)
  }
  ctx.stroke()
  ctx.strokeStyle = '#C00'
  ctx.beginPath()
  ctx.lineTo(x, y)
  for (x = endSeq; x < SEQ; x++) {
    y = hm + Math.round(hm * data[x + start])
    ctx.lineTo(x, y)
  }
  ctx.stroke()
}

},{"../..":3,"./play":2,"audio-loader":5}],2:[function(require,module,exports){

module.exports = function (ac) {
  return function play (when, duration, buffer) {
    var source = ac.createBufferSource()
    source.buffer = buffer
    source.connect(ac.destination)
    source.start(when, 0, duration)
  }
}

},{}],3:[function(require,module,exports){
'use strict'

/**
 * Copy `len` bytes generated by a function to `array` starting at `pos`
 */
function copy (len, array, pos, fn) {
  for (var i = 0; i < len; i++) {
    array[pos + i] = fn(i)
  }
}

function stretch (ac, input, scale, options) {
  // OPTIONS
  var opts = options || {}
  // Processing sequence size (100 msec with 44100Hz sample rate)
  var seqSize = opts.seqSize || 4410
  // Overlapping size (20 msec)
  var overlap = opts.overlap || 882
  // Best overlap offset seeking window (15 msec)
  // var seekWindow = opts.seekWindow || 662

  // The theoretical start of the next sequence
  var nextOffset = Math.round(seqSize / scale)

  // Setup the buffers
  var numSamples = input.length
  var output = ac.createBuffer(1, numSamples * scale, input.sampleRate)
  var inL = input.getChannelData(0)
  var outL = output.getChannelData(0)

  // STATE
  // where to read then next sequence
  var read = 0
  // where to write the next sequence
  var write = 0
  // where to read the next fadeout
  var readOverlap = 0

  while (numSamples - read > seqSize) {
    // write the first overlap
    copy(overlap, outL, write, function (i) {
      var fadeIn = i / overlap
      var fadeOut = 1 - fadeIn
      // Mix the begin of the new sequence with the tail of the sequence last
      return (inL[read + i] * fadeIn + inL[readOverlap + i] * fadeOut) / 2
    })
    copy(seqSize - overlap, outL, write + overlap, function (i) {
      // Copy the tail of the sequence
      return inL[read + overlap + i]
    })
    // the next overlap is after this sequence
    readOverlap += read + seqSize
    // the next sequence is after the nextOffset
    read += nextOffset
    // we wrote a complete sequence
    write += seqSize
  }

  return output
}

module.exports = { stretch: stretch }

},{}],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){
'use strict'

var base64 = require('./base64')
var request = require('./request')

// 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
 *
 * @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
    : isSoundFont(source) ? loadSoundFont
    : null

  var opts = options || {}
  return loader ? loader(ac, source, opts)
    : defVal ? Promise.resolve(defVal)
    : Promise.reject('Source not valid (' + source + ')')
}
load.request = request

// 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.request(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 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.request(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.request(url, 'text').then(midiJsToJson), options)
}

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) + '}')
}

// Load Benjamin Gleitzman's pre-rendered soundfonts via rawgit
var isSoundFont = fromRegex(/^@soundfont\//)
function loadSoundFont (ac, sf, options) {
  var name = sf.slice(sf.indexOf('/'))
  var url = 'https://cdn.rawgit.com/gleitz/midi-js-Soundfonts/master/FluidR3_GM/' + name + '-ogg.js'
  return load(ac, url, options)
}

if (typeof module === 'object' && module.exports) module.exports = load
if (typeof window !== 'undefined') window.loadAudio = load

},{"./base64":4,"./request":6}],6:[function(require,module,exports){
/* global XMLHttpRequest */
'use strict'

// Wrap a XMLHttpRequest into a 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()
  })
}

},{}]},{},[1]);