tone
Version:
A Web Audio framework for making interactive music in the browser.
378 lines • 12.6 kB
JavaScript
import { __awaiter } from "tslib";
import { getContext } from "../Global.js";
import { Tone } from "../Tone.js";
import { optionsFromArguments } from "../util/Defaults.js";
import { noOp } from "../util/Interface.js";
import { isArray, isNumber, isString } from "../util/TypeCheck.js";
import { assert } from "../util/Debug.js";
/**
* AudioBuffer loading and storage. ToneAudioBuffer is used internally by all
* classes that make requests for audio files such as Tone.Player,
* Tone.Sampler and Tone.Convolver.
* @example
* const buffer = new Tone.ToneAudioBuffer("https://tonejs.github.io/audio/casio/A1.mp3", () => {
* console.log("loaded");
* });
* @category Core
*/
export class ToneAudioBuffer extends Tone {
constructor() {
super();
this.name = "ToneAudioBuffer";
/**
* Callback when the buffer is loaded.
*/
this.onload = noOp;
const options = optionsFromArguments(ToneAudioBuffer.getDefaults(), arguments, ["url", "onload", "onerror"]);
this.reverse = options.reverse;
this.onload = options.onload;
if (isString(options.url)) {
// initiate the download
this.load(options.url).catch(options.onerror);
}
else if (options.url) {
this.set(options.url);
}
}
static getDefaults() {
return {
onerror: noOp,
onload: noOp,
reverse: false,
};
}
/**
* The sample rate of the AudioBuffer
*/
get sampleRate() {
if (this._buffer) {
return this._buffer.sampleRate;
}
else {
return getContext().sampleRate;
}
}
/**
* Pass in an AudioBuffer or ToneAudioBuffer to set the value of this buffer.
*/
set(buffer) {
if (buffer instanceof ToneAudioBuffer) {
// if it's loaded, set it
if (buffer.loaded) {
this._buffer = buffer.get();
}
else {
// otherwise when it's loaded, invoke it's callback
buffer.onload = () => {
this.set(buffer);
this.onload(this);
};
}
}
else {
this._buffer = buffer;
}
// reverse it initially
if (this._reversed) {
this._reverse();
}
return this;
}
/**
* The audio buffer stored in the object.
*/
get() {
return this._buffer;
}
/**
* Makes an fetch request for the selected url then decodes the file as an audio buffer.
* Invokes the callback once the audio buffer loads.
* @param url The url of the buffer to load. filetype support depends on the browser.
* @returns A Promise which resolves with this ToneAudioBuffer
*/
load(url) {
return __awaiter(this, void 0, void 0, function* () {
const doneLoading = ToneAudioBuffer.load(url).then((audioBuffer) => {
this.set(audioBuffer);
// invoke the onload method
this.onload(this);
});
ToneAudioBuffer.downloads.push(doneLoading);
try {
yield doneLoading;
}
finally {
// remove the downloaded file
const index = ToneAudioBuffer.downloads.indexOf(doneLoading);
ToneAudioBuffer.downloads.splice(index, 1);
}
return this;
});
}
/**
* clean up
*/
dispose() {
super.dispose();
this._buffer = undefined;
return this;
}
/**
* Set the audio buffer from the array.
* To create a multichannel AudioBuffer, pass in a multidimensional array.
* @param array The array to fill the audio buffer
*/
fromArray(array) {
const isMultidimensional = isArray(array) && array[0].length > 0;
const channels = isMultidimensional ? array.length : 1;
const len = isMultidimensional
? array[0].length
: array.length;
const context = getContext();
const buffer = context.createBuffer(channels, len, context.sampleRate);
const multiChannelArray = !isMultidimensional && channels === 1
? [array]
: array;
for (let c = 0; c < channels; c++) {
buffer.copyToChannel(multiChannelArray[c], c);
}
this._buffer = buffer;
return this;
}
/**
* Sums multiple channels into 1 channel
* @param chanNum Optionally only copy a single channel from the array.
*/
toMono(chanNum) {
if (isNumber(chanNum)) {
this.fromArray(this.toArray(chanNum));
}
else {
let outputArray = new Float32Array(this.length);
const numChannels = this.numberOfChannels;
for (let channel = 0; channel < numChannels; channel++) {
const channelArray = this.toArray(channel);
for (let i = 0; i < channelArray.length; i++) {
outputArray[i] += channelArray[i];
}
}
// divide by the number of channels
outputArray = outputArray.map((sample) => sample / numChannels);
this.fromArray(outputArray);
}
return this;
}
/**
* Get the buffer as an array. Single channel buffers will return a 1-dimensional
* Float32Array, and multichannel buffers will return multidimensional arrays.
* @param channel Optionally only copy a single channel from the array.
*/
toArray(channel) {
if (isNumber(channel)) {
return this.getChannelData(channel);
}
else if (this.numberOfChannels === 1) {
return this.toArray(0);
}
else {
const ret = [];
for (let c = 0; c < this.numberOfChannels; c++) {
ret[c] = this.getChannelData(c);
}
return ret;
}
}
/**
* Returns the Float32Array representing the PCM audio data for the specific channel.
* @param channel The channel number to return
* @return The audio as a TypedArray
*/
getChannelData(channel) {
if (this._buffer) {
return this._buffer.getChannelData(channel);
}
else {
return new Float32Array(0);
}
}
/**
* Cut a subsection of the array and return a buffer of the
* subsection. Does not modify the original buffer
* @param start The time to start the slice
* @param end The end time to slice. If none is given will default to the end of the buffer
*/
slice(start, end = this.duration) {
assert(this.loaded, "Buffer is not loaded");
const startSamples = Math.floor(start * this.sampleRate);
const endSamples = Math.floor(end * this.sampleRate);
assert(startSamples < endSamples, "The start time must be less than the end time");
const length = endSamples - startSamples;
const retBuffer = getContext().createBuffer(this.numberOfChannels, length, this.sampleRate);
for (let channel = 0; channel < this.numberOfChannels; channel++) {
retBuffer.copyToChannel(this.getChannelData(channel).subarray(startSamples, endSamples), channel);
}
return new ToneAudioBuffer(retBuffer);
}
/**
* Reverse the buffer.
*/
_reverse() {
if (this.loaded) {
for (let i = 0; i < this.numberOfChannels; i++) {
this.getChannelData(i).reverse();
}
}
return this;
}
/**
* If the buffer is loaded or not
*/
get loaded() {
return this.length > 0;
}
/**
* The duration of the buffer in seconds.
*/
get duration() {
if (this._buffer) {
return this._buffer.duration;
}
else {
return 0;
}
}
/**
* The length of the buffer in samples
*/
get length() {
if (this._buffer) {
return this._buffer.length;
}
else {
return 0;
}
}
/**
* The number of discrete audio channels. Returns 0 if no buffer is loaded.
*/
get numberOfChannels() {
if (this._buffer) {
return this._buffer.numberOfChannels;
}
else {
return 0;
}
}
/**
* Reverse the buffer.
*/
get reverse() {
return this._reversed;
}
set reverse(rev) {
if (this._reversed !== rev) {
this._reversed = rev;
this._reverse();
}
}
/**
* Create a ToneAudioBuffer from the array. To create a multichannel AudioBuffer,
* pass in a multidimensional array.
* @param array The array to fill the audio buffer
* @return A ToneAudioBuffer created from the array
*/
static fromArray(array) {
return new ToneAudioBuffer().fromArray(array);
}
/**
* Creates a ToneAudioBuffer from a URL, returns a promise which resolves to a ToneAudioBuffer
* @param url The url to load.
* @return A promise which resolves to a ToneAudioBuffer
*/
static fromUrl(url) {
return __awaiter(this, void 0, void 0, function* () {
const buffer = new ToneAudioBuffer();
return yield buffer.load(url);
});
}
/**
* Loads a url using fetch and returns the AudioBuffer.
*/
static load(url) {
return __awaiter(this, void 0, void 0, function* () {
// test if the url contains multiple extensions
const matches = url.match(/\[([^\]\[]+\|.+)\]$/);
if (matches) {
const extensions = matches[1].split("|");
let extension = extensions[0];
for (const ext of extensions) {
if (ToneAudioBuffer.supportsType(ext)) {
extension = ext;
break;
}
}
url = url.replace(matches[0], extension);
}
// make sure there is a slash between the baseUrl and the url
const baseUrl = ToneAudioBuffer.baseUrl === "" ||
ToneAudioBuffer.baseUrl.endsWith("/")
? ToneAudioBuffer.baseUrl
: ToneAudioBuffer.baseUrl + "/";
// encode special characters in file path
const location = document.createElement("a");
location.href = baseUrl + url;
location.pathname = (location.pathname + location.hash)
.split("/")
.map(encodeURIComponent)
.join("/");
const response = yield fetch(location.href);
if (!response.ok) {
throw new Error(`could not load url: ${url}`);
}
const arrayBuffer = yield response.arrayBuffer();
const audioBuffer = yield getContext().decodeAudioData(arrayBuffer);
return audioBuffer;
});
}
/**
* Checks a url's extension to see if the current browser can play that file type.
* @param url The url/extension to test
* @return If the file extension can be played
* @static
* @example
* Tone.ToneAudioBuffer.supportsType("wav"); // returns true
* Tone.ToneAudioBuffer.supportsType("path/to/file.wav"); // returns true
*/
static supportsType(url) {
const extensions = url.split(".");
const extension = extensions[extensions.length - 1];
const response = document
.createElement("audio")
.canPlayType("audio/" + extension);
return response !== "";
}
/**
* Returns a Promise which resolves when all of the buffers have loaded
*/
static loaded() {
return __awaiter(this, void 0, void 0, function* () {
// this makes sure that the function is always async
yield Promise.resolve();
while (ToneAudioBuffer.downloads.length) {
yield ToneAudioBuffer.downloads[0];
}
});
}
}
//-------------------------------------
// STATIC METHODS
//-------------------------------------
/**
* A path which is prefixed before every url.
*/
ToneAudioBuffer.baseUrl = "";
/**
* All of the downloads
*/
ToneAudioBuffer.downloads = [];
//# sourceMappingURL=ToneAudioBuffer.js.map