UNPKG

flocking

Version:

Creative audio synthesis for the Web

496 lines (404 loc) 15 kB
/* * Flocking Audio Buffers * https://github.com/continuing-creativity/flocking * * Copyright 2013-14, Colin Clark * Dual licensed under the MIT and GPL Version 2 licenses. */ /*global require, AudioBuffer*/ /*jshint white: false, newcap: true, regexp: true, browser: true, forin: false, nomen: true, bitwise: false, maxerr: 100, indent: 4, plusplus: false, curly: true, eqeqeq: true, freeze: true, latedef: true, noarg: true, nonew: true, quotmark: double, undef: true, unused: true, strict: true, asi: false, boss: false, evil: false, expr: false, funcscope: false*/ var fluid = fluid || require("infusion"), flock = fluid.registerNamespace("flock"); (function () { "use strict"; // Based on Brian Cavalier and John Hann's Tiny Promises library. // https://github.com/unscriptable/promises/blob/master/src/Tiny2.js function Promise() { /* jshint ignore:start */ var resolve = function (result) { complete("resolve", result); promise.state = "fulfilled"; }; var reject = function (err) { complete("reject", err); promise.state = "rejected"; }; var then = function (resolve, reject) { if (callbacks) { callbacks.push({ resolve: resolve, reject: reject }); } else { var fn = promise.state === "fulfilled" ? resolve : reject; fn(promise.value); } return this; }; var callbacks = [], promise = { state: "pending", value: undefined, resolve: resolve, reject: reject, then: then, safe: { then: function safeThen(resolve, reject) { promise.then(resolve, reject); return this; } } }; function complete(type, result) { var rejector = function (resolve, reject) { reject(result); return this; }; var resolver = function (resolve) { resolve(result); return this; }; promise.value = result; promise.then = type === "reject" ? rejector : resolver; promise.resolve = promise.reject = function () { throw new Error("Promise already completed"); }; invokeCallbacks(type, result); } function invokeCallbacks (type, result) { var i, cb; for (i = 0; i < callbacks.length; i++) { cb = callbacks[i]; if (cb[type]) { cb[type](result); } } callbacks = null; } return promise; /* jshint ignore:end */ } fluid.defaults("flock.promise", { gradeNames: ["fluid.component"], members: { promise: { expander: { funcName: "flock.promise.make" } } } }); flock.promise.make = function () { return new Promise(); }; // TODO: This is actually part of the interpreter's expansion process // and should be clearly named as such. flock.bufferDesc = function (data, sampleRate, numChannels) { var fn = flock.platform.isWebAudio && data instanceof AudioBuffer ? flock.bufferDesc.fromAudioBuffer : flock.isIterable(data) ? flock.bufferDesc.fromChannelArray : flock.bufferDesc.expand; return fn(data, sampleRate, numChannels); }; flock.bufferDesc.inferFormat = function (bufDesc, sampleRate, numChannels) { var format = bufDesc.format, data = bufDesc.data; format.sampleRate = sampleRate || format.sampleRate || 44100; format.numChannels = numChannels || format.numChannels || bufDesc.data.channels.length; format.numSampleFrames = format.numSampleFrames || data.channels.length > 0 ? data.channels[0].length : 0; format.duration = format.numSampleFrames / format.sampleRate; return bufDesc; }; flock.bufferDesc.fromChannelArray = function (arr, sampleRate, numChannels) { if (arr instanceof Float32Array) { arr = [arr]; } var bufDesc = { container: {}, format: { numChannels: numChannels, sampleRate: sampleRate, numSampleFrames: arr[0].length }, data: { channels: arr } }; return flock.bufferDesc.inferFormat(bufDesc, sampleRate, numChannels); }; flock.bufferDesc.expand = function (bufDesc, sampleRate, numChannels) { bufDesc = bufDesc || { data: { channels: [] } }; bufDesc.container = bufDesc.container || {}; bufDesc.format = bufDesc.format || {}; bufDesc.format.numChannels = numChannels || bufDesc.format.numChannels || bufDesc.data.channels.length; // TODO: Duplication with inferFormat. if (bufDesc.data && bufDesc.data.channels) { // Special case for an unwrapped single-channel array. if (bufDesc.format.numChannels === 1 && bufDesc.data.channels.length !== 1) { bufDesc.data.channels = [bufDesc.data.channels]; } if (bufDesc.format.numChannels !== bufDesc.data.channels.length) { throw new Error("The specified number of channels does not match " + "the actual channel data. " + "numChannels was: " + bufDesc.format.numChannels + " but the sample data contains " + bufDesc.data.channels.length + " channels."); } } return flock.bufferDesc.inferFormat(bufDesc, sampleRate, numChannels); }; flock.bufferDesc.fromAudioBuffer = function (audioBuffer) { var desc = { container: {}, format: { sampleRate: audioBuffer.sampleRate, numChannels: audioBuffer.numberOfChannels, numSampleFrames: audioBuffer.length, duration: audioBuffer.duration }, data: { channels: [] } }, i; for (i = 0; i < audioBuffer.numberOfChannels; i++) { desc.data.channels.push(audioBuffer.getChannelData(i)); } return desc; }; flock.bufferDesc.toAudioBuffer = function (context, bufDesc) { var buffer = context.createBuffer(bufDesc.format.numChannels, bufDesc.format.numSampleFrames, bufDesc.format.sampleRate); for (var i = 0; i < bufDesc.format.numChannels; i++) { buffer.copyToChannel(bufDesc.data.channels[i], i); } return buffer; }; /** * Represents a source for fetching buffers. */ fluid.defaults("flock.bufferSource", { gradeNames: ["fluid.modelComponent"], sampleRate: "{enviro}.audioSystem.model.sampleRate", model: { state: "start", src: null }, components: { bufferPromise: { createOnEvent: "onRefreshPromise", type: "flock.promise", options: { listeners: { "onCreate.bindPromiseEvents": { "this": "{that}.promise", method: "then", args: ["{bufferSource}.events.afterFetch.fire", "{bufferSource}.events.onError.fire"] } } } } }, invokers: { get: { funcName: "flock.bufferSource.get", args: ["{that}", "{arguments}.0"] }, set: { funcName: "flock.bufferSource.set", args: ["{that}", "{arguments}.0"] }, error: { funcName: "flock.bufferSource.error", args: ["{that}", "{arguments}.0"] } }, listeners: { "onCreate.fireRefresh": { funcName: "{that}.events.onRefreshPromise.fire" }, "onRefreshPromise.updateState": { changePath: "state", value: "start" }, "onFetch.updateState": { changePath: "state", value: "in-progress" }, "afterFetch.updateState": { changePath: "state", value: "fetched" }, "afterFetch.fireBufferUpdated": { priority: "after:updateState", funcName: "{that}.events.onBufferUpdated.fire", // TODO: Replace with boiling? args: ["{arguments}.0"] }, "onBufferUpdated.registerBuffer": { func: "{enviro}.registerBuffer", args: ["{arguments}.0"], }, "onError.updateState": { changePath: "state", value: "error" } }, events: { onRefreshPromise: null, onError: null, onFetch: null, afterFetch: null, onBufferUpdated: null } }); flock.bufferSource.get = function (that, bufDef) { if (that.model.state === "in-progress" || (bufDef.src === that.model.src && !bufDef.replace)) { // We've already fetched the buffer or are in the process of doing so. return that.bufferPromise.promise; } if (bufDef.src) { if ((that.model.state === "fetched" || that.model.state === "errored") && (that.model.src !== bufDef.src || bufDef.replace)) { that.events.onRefreshPromise.fire(); } if (that.model.state === "start") { that.model.src = bufDef.src; that.events.onFetch.fire(bufDef); flock.audio.decode({ src: bufDef.src, sampleRate: that.options.sampleRate, success: function (bufDesc) { if (bufDef.id) { bufDesc.id = bufDef.id; } that.set(bufDesc); }, error: that.error }); } } return that.bufferPromise.promise; }; flock.bufferSource.set = function (that, bufDesc) { var state = that.model.state; if (state === "start" || state === "in-progress") { that.bufferPromise.promise.resolve(bufDesc); } return that.bufferPromise.promise; }; flock.bufferSource.error = function (that, msg) { that.bufferPromise.promise.reject(msg); return that.bufferPromise.promise; }; /** * A Buffer Loader is responsible for loading a collection * of buffers asynchronously, and will fire an event when they * are all ready. */ fluid.defaults("flock.bufferLoader", { gradeNames: ["fluid.component"], // A list of BufferDef objects to resolve. bufferDefs: [], members: { buffers: [], bufferDefs: "@expand:flock.bufferLoader.expandBufferDefs({that}.options.bufferDefs)" }, components: { enviro: "{flock.enviro}" }, events: { afterBuffersLoaded: null, onError: null }, listeners: { "onCreate.loadBuffers": { funcName: "flock.bufferLoader.loadBuffers", args: ["{that}"] }, "onError.logError": { funcName: "flock.log.fail" } } }); flock.bufferLoader.idFromURL = function (url) { var lastSlash = url.lastIndexOf("/"), idStart = lastSlash > -1 ? lastSlash + 1 : 0, ext = url.lastIndexOf("."), idEnd = ext > -1 ? ext : url.length; return url.substring(idStart, idEnd); }; flock.bufferLoader.idsFromURLs = function (urls) { return fluid.transform(urls, flock.bufferLoader.idFromURL); }; flock.bufferLoader.expandFileSequence = function (fileURLs) { fileURLs = fileURLs || []; var bufDefs = [], i, url, id; for (i = 0; i < fileURLs.length; i++) { url = fileURLs[i]; id = flock.bufferLoader.idFromURL(url); bufDefs.push({ id: id, url: url }); } return bufDefs; }; // TODO: Resolve this with the expansion logic in the interpeter. // This operates similar but conflicting logic; strings are expanded as URLs // instead of IDs. flock.bufferLoader.expandBufferDef = function (bufDef) { if (typeof bufDef === "string") { bufDef = { url: bufDef }; } if (bufDef.id === undefined && bufDef.url !== undefined) { bufDef.id = flock.bufferLoader.idFromURL(bufDef.url); } return bufDef; }; flock.bufferLoader.expandBufferDefs = function (bufferDefs) { if (!bufferDefs) { return []; } bufferDefs = fluid.makeArray(bufferDefs); return fluid.transform(bufferDefs, flock.bufferLoader.expandBufferDef); }; flock.bufferLoader.loadBuffer = function (bufDef, bufferTarget, that) { try { flock.parse.bufferForDef(bufDef, bufferTarget, that.enviro); } catch (e) { that.events.onError.fire(e.message); } }; flock.bufferLoader.loadBuffers = function (that) { var bufferDefIdx = 1; // TODO: This is a sign that flock.parse.bufferForDef is still terribly broken. var bufferTarget = { setBuffer: function (decoded) { that.buffers.push(decoded); // TODO: This is not robust and provides no means for error notification! if (that.buffers.length === that.options.bufferDefs.length) { that.events.afterBuffersLoaded.fire(that.buffers); } else if (bufferDefIdx < that.bufferDefs.length){ var nextBufferDef = that.bufferDefs[bufferDefIdx]; flock.bufferLoader.loadBuffer(nextBufferDef, bufferTarget, that); bufferDefIdx++; } } }; flock.bufferLoader.loadBuffer(that.bufferDefs[0], bufferTarget, that); }; }());