audiojs/audio

View on GitHub
src/core.js

Summary

Maintainability
D
1 day
Test Coverage
/**
 * Core Audio methods
 */

'use strict'


const loadAudio = require('audio-loader')
const decodeAudio = require('audio-decode')
const extend = require('object-assign')
const nidx = require('negative-index')
const isPromise = require('is-promise')
const saveAs = require('save-file')
const toWav = require('audiobuffer-to-wav')
const AudioBuffer = require('audio-buffer')
const AudioBufferList = require('audio-buffer-list')
const remix = require('audio-buffer-remix')
const isAudioBuffer = require('is-audio-buffer')
const isPlainObj = require('is-plain-obj')
const getContext = require('audio-context')
const convert = require('pcm-convert')
const aformat = require('audio-format')
const createBuffer = require('audio-buffer-from')

let {parseArgs, resolvePath, isMultisource} = require('./util')
let Audio = require('../')


// cache of loaded audio buffers for urls
Audio.cache = {}


// cache URLs
Audio.prototype.cache = true


// enable metrics
Audio.prototype.stats = false

// default params
Object.defineProperties(Audio.prototype, {
    channels: {
        set: function (channels) {
            this.buffer = remix(this.buffer, this.buffer.numberOfChannels, channels)
        },
        get: function () {
            return this.buffer.numberOfChannels
        }
    },
    sampleRate: {
        set: function () {
            // TODO
            throw Error('Unimplemented.')
        },
        get: function () {
            return this.buffer.sampleRate || 44100
        }
    },
    duration: {
        set: function (duration) {
            let length = Math.floor(duration * this.sampleRate)
            if (length < this.length) {
                this.buffer = this.buffer.slice(0, length)
            }
            else if (length > this.length) {
                this.buffer = this.pad(duration, {right: true})
            }
        },
        get: function () {
            return this.buffer.duration
        }
    },
    length: {
        set: function (length) {
            if (length < this.length) {
                this.buffer = this.buffer.slice(0, length)
            }
            else if (length > this.length) {
                // TODO
                // this.buffer = this.pad({start: , right: true})
            }
        },
        get: function () {
            return this.buffer.length
        }
    }
})


// create audio from multiple sources
Audio.from = function from (...sources) {
    let items = [], channels = 1

    let options = sources[sources.length - 1]

    if ((isPlainObj(options) && (!options.duration || !options.length)) || typeof options === 'string' ) {
        sources.pop()
    }
    else {
        options = null
    }

    for (let i = 0; i < sources.length; i++) {
        let source = sources[i], subsource

        //multiple source
        if (isMultisource(source)) {
            if (options) {
                subsource = Audio.from(...source, options).buffer
            } else {
                subsource = Audio.from(...source).buffer
            }
        }
        else {
            subsource = source instanceof Audio ? source.buffer : Audio(source, options).buffer
        }
        items.push(subsource)
        channels = Math.max(subsource.numberOfChannels, channels)
    }

    let buffer = new AudioBufferList(items, {numberOfChannels: channels, sampleRate: items[0].sampleRate})

    return new Audio(buffer)
}


// load audio from remote/local url
Audio.load = function load (source, callback) {
    let promise

    if (typeof source === 'string') {
        source = resolvePath(source, 2)

        // load cached version, if any
        if (Audio.cache[source]) {
            // if source is cached but loading - just clone when loaded
            if (isPromise(Audio.cache[source])) {
                promise = Audio.cache[source].then(audio => {
                    // in order to avoid fetching modified source, we clone cached source
                    audio = Audio(Audio.cache[source])

                    callback && callback(null, audio)
                    return Promise.resolve(audio)
                }, error => {
                    callback && callback(error)
                    return Promise.reject(error)
                })
            }
            // else clone right ahead
            else {
                promise = Promise.resolve(Audio(Audio.cache[source])).then(audio => {
                    callback && callback(null, audio)
                    return Promise.resolve(audio)
                }, error => {
                    callback && callback(error)
                    return Promise.reject(error)
                })
            }
        }

        // load source by path
        else {
            promise = loadAudio(source).then(audioBuffer => {
                let audio = Audio(audioBuffer)

                // since user may modify original audio later, we have to put clone into cache
                Audio.cache[source] = Audio(audio)

                callback && callback(null, audio)
                return Promise.resolve(audio)
            }, error => {
                callback && callback(error)
                return Promise.reject(error)
            })

            // save promise to cache
            Audio.cache[source] = promise
        }
    }

    // multiple sources
    else if (Array.isArray(source)) {
        let items = []

        // make sure for every array item audio instance is created and loaded
        for (let i = 0; i < source.length; i++) {
            let a = source[i]
            if (typeof a === 'string') {
                a = resolvePath(a, 2)
                items[i] = Audio.load(a)
            }
            else if (isPromise(a)) {
                items[i] = a
            }
            else {
                items[i] = Promise.resolve(Audio(a))
            }
        }

        // then do promise once all loaded
        promise = Promise.all(items).then((list) => {
            callback && callback(null, list)
            return Promise.resolve(list)
        }, error => {
            callback && callback(error)
            return Promise.reject(error)
        })
    }

    // fall back non-string sources to decode
    else {
        promise = Audio.decode(source, callback)
    }

    return promise
}


// decode audio buffer
Audio.decode = function decode (source, options, callback) {
    if (typeof options === 'function') {
        callback = options
        options = {context: this.context}
    }

    if (!source) throw Error('No source to decode');

    // decode multiple items
    if (Array.isArray(source)) {
        let items = []

        // make sure for every array item audio instance is created and loaded
        for (let i = 0; i < source.length; i++) {
            let a = source[i]
            if (isPromise(a)) {
                items[i] = a
            }
            else {
                items[i] = Audio.decode(a)
            }
        }

        // then do promise once all loaded
        return Promise.all(items).then((list) => {
            callback && callback(null, list)
            return Promise.resolve(list)
        }, error => {
            callback && callback(error)
            return Promise.reject(error)
        })
    }

    // convert to AudioBuffer
    return decodeAudio(source, options).then(
        audioBuffer => {
            let audio = Audio(audioBuffer)
            callback && callback(null, audio)
            return audio
        },
        error => {
            callback && callback(error)
            return Promise.reject(error)
        }
    )
}


// record streamish source
Audio.record = function record (source, options, callback) {

}


// download file or create a file in node
Audio.prototype.save = function save (fileName, ondone) {
    if (!fileName) throw Error('File name is not provided')

    let buffer = this.read({format: 'audiobuffer'})

    let wav = toWav(buffer)

    // fix path for node
    fileName = resolvePath(fileName)

    saveAs(wav, fileName, (err) => {
        ondone && ondone(err, this)
    })

    return this
}

// test if audio is equal
Audio.equal = function (a, ...sources) {
    for (let i = 0; i < sources.length; i++) {
        let b = sources[i]

        if (a === b) return true
        if (a.length !== b.length || a.channels !== b.channels || a.sampleRate != b.sampleRate) return false


        for (let c = 0; c < a.channels; c++) {
            let dataA = a.read({channel: c});
            let dataB = b.read({channel: c});

            for (let i = 0; i < dataA.length; i++) {
                if (dataA[i] !== dataB[i]) return false;
            }
        }
    }

    return true
}