UNPKG

12.6 kBJavaScriptView Raw
1import { __awaiter } from "tslib";
2import { getContext } from "../Global.js";
3import { Tone } from "../Tone.js";
4import { optionsFromArguments } from "../util/Defaults.js";
5import { noOp } from "../util/Interface.js";
6import { isArray, isNumber, isString } from "../util/TypeCheck.js";
7import { assert } from "../util/Debug.js";
8/**
9 * AudioBuffer loading and storage. ToneAudioBuffer is used internally by all
10 * classes that make requests for audio files such as Tone.Player,
11 * Tone.Sampler and Tone.Convolver.
12 * @example
13 * const buffer = new Tone.ToneAudioBuffer("https://tonejs.github.io/audio/casio/A1.mp3", () => {
14 * console.log("loaded");
15 * });
16 * @category Core
17 */
18export class ToneAudioBuffer extends Tone {
19 constructor() {
20 super();
21 this.name = "ToneAudioBuffer";
22 /**
23 * Callback when the buffer is loaded.
24 */
25 this.onload = noOp;
26 const options = optionsFromArguments(ToneAudioBuffer.getDefaults(), arguments, ["url", "onload", "onerror"]);
27 this.reverse = options.reverse;
28 this.onload = options.onload;
29 if (isString(options.url)) {
30 // initiate the download
31 this.load(options.url).catch(options.onerror);
32 }
33 else if (options.url) {
34 this.set(options.url);
35 }
36 }
37 static getDefaults() {
38 return {
39 onerror: noOp,
40 onload: noOp,
41 reverse: false,
42 };
43 }
44 /**
45 * The sample rate of the AudioBuffer
46 */
47 get sampleRate() {
48 if (this._buffer) {
49 return this._buffer.sampleRate;
50 }
51 else {
52 return getContext().sampleRate;
53 }
54 }
55 /**
56 * Pass in an AudioBuffer or ToneAudioBuffer to set the value of this buffer.
57 */
58 set(buffer) {
59 if (buffer instanceof ToneAudioBuffer) {
60 // if it's loaded, set it
61 if (buffer.loaded) {
62 this._buffer = buffer.get();
63 }
64 else {
65 // otherwise when it's loaded, invoke it's callback
66 buffer.onload = () => {
67 this.set(buffer);
68 this.onload(this);
69 };
70 }
71 }
72 else {
73 this._buffer = buffer;
74 }
75 // reverse it initially
76 if (this._reversed) {
77 this._reverse();
78 }
79 return this;
80 }
81 /**
82 * The audio buffer stored in the object.
83 */
84 get() {
85 return this._buffer;
86 }
87 /**
88 * Makes an fetch request for the selected url then decodes the file as an audio buffer.
89 * Invokes the callback once the audio buffer loads.
90 * @param url The url of the buffer to load. filetype support depends on the browser.
91 * @returns A Promise which resolves with this ToneAudioBuffer
92 */
93 load(url) {
94 return __awaiter(this, void 0, void 0, function* () {
95 const doneLoading = ToneAudioBuffer.load(url).then((audioBuffer) => {
96 this.set(audioBuffer);
97 // invoke the onload method
98 this.onload(this);
99 });
100 ToneAudioBuffer.downloads.push(doneLoading);
101 try {
102 yield doneLoading;
103 }
104 finally {
105 // remove the downloaded file
106 const index = ToneAudioBuffer.downloads.indexOf(doneLoading);
107 ToneAudioBuffer.downloads.splice(index, 1);
108 }
109 return this;
110 });
111 }
112 /**
113 * clean up
114 */
115 dispose() {
116 super.dispose();
117 this._buffer = undefined;
118 return this;
119 }
120 /**
121 * Set the audio buffer from the array.
122 * To create a multichannel AudioBuffer, pass in a multidimensional array.
123 * @param array The array to fill the audio buffer
124 */
125 fromArray(array) {
126 const isMultidimensional = isArray(array) && array[0].length > 0;
127 const channels = isMultidimensional ? array.length : 1;
128 const len = isMultidimensional
129 ? array[0].length
130 : array.length;
131 const context = getContext();
132 const buffer = context.createBuffer(channels, len, context.sampleRate);
133 const multiChannelArray = !isMultidimensional && channels === 1
134 ? [array]
135 : array;
136 for (let c = 0; c < channels; c++) {
137 buffer.copyToChannel(multiChannelArray[c], c);
138 }
139 this._buffer = buffer;
140 return this;
141 }
142 /**
143 * Sums multiple channels into 1 channel
144 * @param chanNum Optionally only copy a single channel from the array.
145 */
146 toMono(chanNum) {
147 if (isNumber(chanNum)) {
148 this.fromArray(this.toArray(chanNum));
149 }
150 else {
151 let outputArray = new Float32Array(this.length);
152 const numChannels = this.numberOfChannels;
153 for (let channel = 0; channel < numChannels; channel++) {
154 const channelArray = this.toArray(channel);
155 for (let i = 0; i < channelArray.length; i++) {
156 outputArray[i] += channelArray[i];
157 }
158 }
159 // divide by the number of channels
160 outputArray = outputArray.map((sample) => sample / numChannels);
161 this.fromArray(outputArray);
162 }
163 return this;
164 }
165 /**
166 * Get the buffer as an array. Single channel buffers will return a 1-dimensional
167 * Float32Array, and multichannel buffers will return multidimensional arrays.
168 * @param channel Optionally only copy a single channel from the array.
169 */
170 toArray(channel) {
171 if (isNumber(channel)) {
172 return this.getChannelData(channel);
173 }
174 else if (this.numberOfChannels === 1) {
175 return this.toArray(0);
176 }
177 else {
178 const ret = [];
179 for (let c = 0; c < this.numberOfChannels; c++) {
180 ret[c] = this.getChannelData(c);
181 }
182 return ret;
183 }
184 }
185 /**
186 * Returns the Float32Array representing the PCM audio data for the specific channel.
187 * @param channel The channel number to return
188 * @return The audio as a TypedArray
189 */
190 getChannelData(channel) {
191 if (this._buffer) {
192 return this._buffer.getChannelData(channel);
193 }
194 else {
195 return new Float32Array(0);
196 }
197 }
198 /**
199 * Cut a subsection of the array and return a buffer of the
200 * subsection. Does not modify the original buffer
201 * @param start The time to start the slice
202 * @param end The end time to slice. If none is given will default to the end of the buffer
203 */
204 slice(start, end = this.duration) {
205 assert(this.loaded, "Buffer is not loaded");
206 const startSamples = Math.floor(start * this.sampleRate);
207 const endSamples = Math.floor(end * this.sampleRate);
208 assert(startSamples < endSamples, "The start time must be less than the end time");
209 const length = endSamples - startSamples;
210 const retBuffer = getContext().createBuffer(this.numberOfChannels, length, this.sampleRate);
211 for (let channel = 0; channel < this.numberOfChannels; channel++) {
212 retBuffer.copyToChannel(this.getChannelData(channel).subarray(startSamples, endSamples), channel);
213 }
214 return new ToneAudioBuffer(retBuffer);
215 }
216 /**
217 * Reverse the buffer.
218 */
219 _reverse() {
220 if (this.loaded) {
221 for (let i = 0; i < this.numberOfChannels; i++) {
222 this.getChannelData(i).reverse();
223 }
224 }
225 return this;
226 }
227 /**
228 * If the buffer is loaded or not
229 */
230 get loaded() {
231 return this.length > 0;
232 }
233 /**
234 * The duration of the buffer in seconds.
235 */
236 get duration() {
237 if (this._buffer) {
238 return this._buffer.duration;
239 }
240 else {
241 return 0;
242 }
243 }
244 /**
245 * The length of the buffer in samples
246 */
247 get length() {
248 if (this._buffer) {
249 return this._buffer.length;
250 }
251 else {
252 return 0;
253 }
254 }
255 /**
256 * The number of discrete audio channels. Returns 0 if no buffer is loaded.
257 */
258 get numberOfChannels() {
259 if (this._buffer) {
260 return this._buffer.numberOfChannels;
261 }
262 else {
263 return 0;
264 }
265 }
266 /**
267 * Reverse the buffer.
268 */
269 get reverse() {
270 return this._reversed;
271 }
272 set reverse(rev) {
273 if (this._reversed !== rev) {
274 this._reversed = rev;
275 this._reverse();
276 }
277 }
278 /**
279 * Create a ToneAudioBuffer from the array. To create a multichannel AudioBuffer,
280 * pass in a multidimensional array.
281 * @param array The array to fill the audio buffer
282 * @return A ToneAudioBuffer created from the array
283 */
284 static fromArray(array) {
285 return new ToneAudioBuffer().fromArray(array);
286 }
287 /**
288 * Creates a ToneAudioBuffer from a URL, returns a promise which resolves to a ToneAudioBuffer
289 * @param url The url to load.
290 * @return A promise which resolves to a ToneAudioBuffer
291 */
292 static fromUrl(url) {
293 return __awaiter(this, void 0, void 0, function* () {
294 const buffer = new ToneAudioBuffer();
295 return yield buffer.load(url);
296 });
297 }
298 /**
299 * Loads a url using fetch and returns the AudioBuffer.
300 */
301 static load(url) {
302 return __awaiter(this, void 0, void 0, function* () {
303 // test if the url contains multiple extensions
304 const matches = url.match(/\[([^\]\[]+\|.+)\]$/);
305 if (matches) {
306 const extensions = matches[1].split("|");
307 let extension = extensions[0];
308 for (const ext of extensions) {
309 if (ToneAudioBuffer.supportsType(ext)) {
310 extension = ext;
311 break;
312 }
313 }
314 url = url.replace(matches[0], extension);
315 }
316 // make sure there is a slash between the baseUrl and the url
317 const baseUrl = ToneAudioBuffer.baseUrl === "" ||
318 ToneAudioBuffer.baseUrl.endsWith("/")
319 ? ToneAudioBuffer.baseUrl
320 : ToneAudioBuffer.baseUrl + "/";
321 // encode special characters in file path
322 const location = document.createElement("a");
323 location.href = baseUrl + url;
324 location.pathname = (location.pathname + location.hash)
325 .split("/")
326 .map(encodeURIComponent)
327 .join("/");
328 const response = yield fetch(location.href);
329 if (!response.ok) {
330 throw new Error(`could not load url: ${url}`);
331 }
332 const arrayBuffer = yield response.arrayBuffer();
333 const audioBuffer = yield getContext().decodeAudioData(arrayBuffer);
334 return audioBuffer;
335 });
336 }
337 /**
338 * Checks a url's extension to see if the current browser can play that file type.
339 * @param url The url/extension to test
340 * @return If the file extension can be played
341 * @static
342 * @example
343 * Tone.ToneAudioBuffer.supportsType("wav"); // returns true
344 * Tone.ToneAudioBuffer.supportsType("path/to/file.wav"); // returns true
345 */
346 static supportsType(url) {
347 const extensions = url.split(".");
348 const extension = extensions[extensions.length - 1];
349 const response = document
350 .createElement("audio")
351 .canPlayType("audio/" + extension);
352 return response !== "";
353 }
354 /**
355 * Returns a Promise which resolves when all of the buffers have loaded
356 */
357 static loaded() {
358 return __awaiter(this, void 0, void 0, function* () {
359 // this makes sure that the function is always async
360 yield Promise.resolve();
361 while (ToneAudioBuffer.downloads.length) {
362 yield ToneAudioBuffer.downloads[0];
363 }
364 });
365 }
366}
367//-------------------------------------
368// STATIC METHODS
369//-------------------------------------
370/**
371 * A path which is prefixed before every url.
372 */
373ToneAudioBuffer.baseUrl = "";
374/**
375 * All of the downloads
376 */
377ToneAudioBuffer.downloads = [];
378//# sourceMappingURL=ToneAudioBuffer.js.map
\No newline at end of file