1 | /* |
---|
2 | * Licensed to the Apache Software Foundation (ASF) under one |
---|
3 | * or more contributor license agreements. See the NOTICE file |
---|
4 | * distributed with this work for additional information |
---|
5 | * regarding copyright ownership. The ASF licenses this file |
---|
6 | * to you under the Apache License, Version 2.0 (the |
---|
7 | * "License"); you may not use this file except in compliance |
---|
8 | * with the License. You may obtain a copy of the License at |
---|
9 | * |
---|
10 | * http://www.apache.org/licenses/LICENSE-2.0 |
---|
11 | * |
---|
12 | * Unless required by applicable law or agreed to in writing, |
---|
13 | * software distributed under the License is distributed on an |
---|
14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
---|
15 | * KIND, either express or implied. See the License for the |
---|
16 | * specific language governing permissions and limitations |
---|
17 | * under the License. |
---|
18 | */ |
---|
19 | |
---|
20 | var Guacamole = Guacamole || {}; |
---|
21 | |
---|
22 | /** |
---|
23 | * Abstract audio player which accepts, queues and plays back arbitrary audio |
---|
24 | * data. It is up to implementations of this class to provide some means of |
---|
25 | * handling a provided Guacamole.InputStream. Data received along the provided |
---|
26 | * stream is to be played back immediately. |
---|
27 | * |
---|
28 | * @constructor |
---|
29 | */ |
---|
30 | Guacamole.AudioPlayer = function AudioPlayer() { |
---|
31 | |
---|
32 | /** |
---|
33 | * Notifies this Guacamole.AudioPlayer that all audio up to the current |
---|
34 | * point in time has been given via the underlying stream, and that any |
---|
35 | * difference in time between queued audio data and the current time can be |
---|
36 | * considered latency. |
---|
37 | */ |
---|
38 | this.sync = function sync() { |
---|
39 | // Default implementation - do nothing |
---|
40 | }; |
---|
41 | |
---|
42 | }; |
---|
43 | |
---|
44 | /** |
---|
45 | * Determines whether the given mimetype is supported by any built-in |
---|
46 | * implementation of Guacamole.AudioPlayer, and thus will be properly handled |
---|
47 | * by Guacamole.AudioPlayer.getInstance(). |
---|
48 | * |
---|
49 | * @param {String} mimetype |
---|
50 | * The mimetype to check. |
---|
51 | * |
---|
52 | * @returns {Boolean} |
---|
53 | * true if the given mimetype is supported by any built-in |
---|
54 | * Guacamole.AudioPlayer, false otherwise. |
---|
55 | */ |
---|
56 | Guacamole.AudioPlayer.isSupportedType = function isSupportedType(mimetype) { |
---|
57 | |
---|
58 | return Guacamole.RawAudioPlayer.isSupportedType(mimetype); |
---|
59 | |
---|
60 | }; |
---|
61 | |
---|
62 | /** |
---|
63 | * Returns a list of all mimetypes supported by any built-in |
---|
64 | * Guacamole.AudioPlayer, in rough order of priority. Beware that only the core |
---|
65 | * mimetypes themselves will be listed. Any mimetype parameters, even required |
---|
66 | * ones, will not be included in the list. For example, "audio/L8" is a |
---|
67 | * supported raw audio mimetype that is supported, but it is invalid without |
---|
68 | * additional parameters. Something like "audio/L8;rate=44100" would be valid, |
---|
69 | * however (see https://tools.ietf.org/html/rfc4856). |
---|
70 | * |
---|
71 | * @returns {String[]} |
---|
72 | * A list of all mimetypes supported by any built-in Guacamole.AudioPlayer, |
---|
73 | * excluding any parameters. |
---|
74 | */ |
---|
75 | Guacamole.AudioPlayer.getSupportedTypes = function getSupportedTypes() { |
---|
76 | |
---|
77 | return Guacamole.RawAudioPlayer.getSupportedTypes(); |
---|
78 | |
---|
79 | }; |
---|
80 | |
---|
81 | /** |
---|
82 | * Returns an instance of Guacamole.AudioPlayer providing support for the given |
---|
83 | * audio format. If support for the given audio format is not available, null |
---|
84 | * is returned. |
---|
85 | * |
---|
86 | * @param {Guacamole.InputStream} stream |
---|
87 | * The Guacamole.InputStream to read audio data from. |
---|
88 | * |
---|
89 | * @param {String} mimetype |
---|
90 | * The mimetype of the audio data in the provided stream. |
---|
91 | * |
---|
92 | * @return {Guacamole.AudioPlayer} |
---|
93 | * A Guacamole.AudioPlayer instance supporting the given mimetype and |
---|
94 | * reading from the given stream, or null if support for the given mimetype |
---|
95 | * is absent. |
---|
96 | */ |
---|
97 | Guacamole.AudioPlayer.getInstance = function getInstance(stream, mimetype) { |
---|
98 | |
---|
99 | // Use raw audio player if possible |
---|
100 | if (Guacamole.RawAudioPlayer.isSupportedType(mimetype)) |
---|
101 | return new Guacamole.RawAudioPlayer(stream, mimetype); |
---|
102 | |
---|
103 | // No support for given mimetype |
---|
104 | return null; |
---|
105 | |
---|
106 | }; |
---|
107 | |
---|
108 | /** |
---|
109 | * Implementation of Guacamole.AudioPlayer providing support for raw PCM format |
---|
110 | * audio. This player relies only on the Web Audio API and does not require any |
---|
111 | * browser-level support for its audio formats. |
---|
112 | * |
---|
113 | * @constructor |
---|
114 | * @augments Guacamole.AudioPlayer |
---|
115 | * @param {Guacamole.InputStream} stream |
---|
116 | * The Guacamole.InputStream to read audio data from. |
---|
117 | * |
---|
118 | * @param {String} mimetype |
---|
119 | * The mimetype of the audio data in the provided stream, which must be a |
---|
120 | * "audio/L8" or "audio/L16" mimetype with necessary parameters, such as: |
---|
121 | * "audio/L16;rate=44100,channels=2". |
---|
122 | */ |
---|
123 | Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { |
---|
124 | |
---|
125 | /** |
---|
126 | * The format of audio this player will decode. |
---|
127 | * |
---|
128 | * @private |
---|
129 | * @type {Guacamole.RawAudioFormat} |
---|
130 | */ |
---|
131 | var format = Guacamole.RawAudioFormat.parse(mimetype); |
---|
132 | |
---|
133 | /** |
---|
134 | * An instance of a Web Audio API AudioContext object, or null if the |
---|
135 | * Web Audio API is not supported. |
---|
136 | * |
---|
137 | * @private |
---|
138 | * @type {AudioContext} |
---|
139 | */ |
---|
140 | var context = Guacamole.AudioContextFactory.getAudioContext(); |
---|
141 | |
---|
142 | /** |
---|
143 | * The earliest possible time that the next packet could play without |
---|
144 | * overlapping an already-playing packet, in seconds. Note that while this |
---|
145 | * value is in seconds, it is not an integer value and has microsecond |
---|
146 | * resolution. |
---|
147 | * |
---|
148 | * @private |
---|
149 | * @type {Number} |
---|
150 | */ |
---|
151 | var nextPacketTime = context.currentTime; |
---|
152 | |
---|
153 | /** |
---|
154 | * Guacamole.ArrayBufferReader wrapped around the audio input stream |
---|
155 | * provided with this Guacamole.RawAudioPlayer was created. |
---|
156 | * |
---|
157 | * @private |
---|
158 | * @type {Guacamole.ArrayBufferReader} |
---|
159 | */ |
---|
160 | var reader = new Guacamole.ArrayBufferReader(stream); |
---|
161 | |
---|
162 | /** |
---|
163 | * The minimum size of an audio packet split by splitAudioPacket(), in |
---|
164 | * seconds. Audio packets smaller than this will not be split, nor will the |
---|
165 | * split result of a larger packet ever be smaller in size than this |
---|
166 | * minimum. |
---|
167 | * |
---|
168 | * @private |
---|
169 | * @constant |
---|
170 | * @type {Number} |
---|
171 | */ |
---|
172 | var MIN_SPLIT_SIZE = 0.02; |
---|
173 | |
---|
174 | /** |
---|
175 | * The maximum amount of latency to allow between the buffered data stream |
---|
176 | * and the playback position, in seconds. Initially, this is set to |
---|
177 | * roughly one third of a second. |
---|
178 | * |
---|
179 | * @private |
---|
180 | * @type {Number} |
---|
181 | */ |
---|
182 | var maxLatency = 0.3; |
---|
183 | |
---|
184 | /** |
---|
185 | * The type of typed array that will be used to represent each audio packet |
---|
186 | * internally. This will be either Int8Array or Int16Array, depending on |
---|
187 | * whether the raw audio format is 8-bit or 16-bit. |
---|
188 | * |
---|
189 | * @private |
---|
190 | * @constructor |
---|
191 | */ |
---|
192 | var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array; |
---|
193 | |
---|
194 | /** |
---|
195 | * The maximum absolute value of any sample within a raw audio packet |
---|
196 | * received by this audio player. This depends only on the size of each |
---|
197 | * sample, and will be 128 for 8-bit audio and 32768 for 16-bit audio. |
---|
198 | * |
---|
199 | * @private |
---|
200 | * @type {Number} |
---|
201 | */ |
---|
202 | var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768; |
---|
203 | |
---|
204 | /** |
---|
205 | * The queue of all pending audio packets, as an array of sample arrays. |
---|
206 | * Audio packets which are pending playback will be added to this queue for |
---|
207 | * further manipulation prior to scheduling via the Web Audio API. Once an |
---|
208 | * audio packet leaves this queue and is scheduled via the Web Audio API, |
---|
209 | * no further modifications can be made to that packet. |
---|
210 | * |
---|
211 | * @private |
---|
212 | * @type {SampleArray[]} |
---|
213 | */ |
---|
214 | var packetQueue = []; |
---|
215 | |
---|
216 | /** |
---|
217 | * Given an array of audio packets, returns a single audio packet |
---|
218 | * containing the concatenation of those packets. |
---|
219 | * |
---|
220 | * @private |
---|
221 | * @param {SampleArray[]} packets |
---|
222 | * The array of audio packets to concatenate. |
---|
223 | * |
---|
224 | * @returns {SampleArray} |
---|
225 | * A single audio packet containing the concatenation of all given |
---|
226 | * audio packets. If no packets are provided, this will be undefined. |
---|
227 | */ |
---|
228 | var joinAudioPackets = function joinAudioPackets(packets) { |
---|
229 | |
---|
230 | // Do not bother joining if one or fewer packets are in the queue |
---|
231 | if (packets.length <= 1) |
---|
232 | return packets[0]; |
---|
233 | |
---|
234 | // Determine total sample length of the entire queue |
---|
235 | var totalLength = 0; |
---|
236 | packets.forEach(function addPacketLengths(packet) { |
---|
237 | totalLength += packet.length; |
---|
238 | }); |
---|
239 | |
---|
240 | // Append each packet within queue |
---|
241 | var offset = 0; |
---|
242 | var joined = new SampleArray(totalLength); |
---|
243 | packets.forEach(function appendPacket(packet) { |
---|
244 | joined.set(packet, offset); |
---|
245 | offset += packet.length; |
---|
246 | }); |
---|
247 | |
---|
248 | return joined; |
---|
249 | |
---|
250 | }; |
---|
251 | |
---|
252 | /** |
---|
253 | * Given a single packet of audio data, splits off an arbitrary length of |
---|
254 | * audio data from the beginning of that packet, returning the split result |
---|
255 | * as an array of two packets. The split location is determined through an |
---|
256 | * algorithm intended to minimize the liklihood of audible clicking between |
---|
257 | * packets. If no such split location is possible, an array containing only |
---|
258 | * the originally-provided audio packet is returned. |
---|
259 | * |
---|
260 | * @private |
---|
261 | * @param {SampleArray} data |
---|
262 | * The audio packet to split. |
---|
263 | * |
---|
264 | * @returns {SampleArray[]} |
---|
265 | * An array of audio packets containing the result of splitting the |
---|
266 | * provided audio packet. If splitting is possible, this array will |
---|
267 | * contain two packets. If splitting is not possible, this array will |
---|
268 | * contain only the originally-provided packet. |
---|
269 | */ |
---|
270 | var splitAudioPacket = function splitAudioPacket(data) { |
---|
271 | |
---|
272 | var minValue = Number.MAX_VALUE; |
---|
273 | var optimalSplitLength = data.length; |
---|
274 | |
---|
275 | // Calculate number of whole samples in the provided audio packet AND |
---|
276 | // in the minimum possible split packet |
---|
277 | var samples = Math.floor(data.length / format.channels); |
---|
278 | var minSplitSamples = Math.floor(format.rate * MIN_SPLIT_SIZE); |
---|
279 | |
---|
280 | // Calculate the beginning of the "end" of the audio packet |
---|
281 | var start = Math.max( |
---|
282 | format.channels * minSplitSamples, |
---|
283 | format.channels * (samples - minSplitSamples) |
---|
284 | ); |
---|
285 | |
---|
286 | // For all samples at the end of the given packet, find a point where |
---|
287 | // the perceptible volume across all channels is lowest (and thus is |
---|
288 | // the optimal point to split) |
---|
289 | for (var offset = start; offset < data.length; offset += format.channels) { |
---|
290 | |
---|
291 | // Calculate the sum of all values across all channels (the result |
---|
292 | // will be proportional to the average volume of a sample) |
---|
293 | var totalValue = 0; |
---|
294 | for (var channel = 0; channel < format.channels; channel++) { |
---|
295 | totalValue += Math.abs(data[offset + channel]); |
---|
296 | } |
---|
297 | |
---|
298 | // If this is the smallest average value thus far, set the split |
---|
299 | // length such that the first packet ends with the current sample |
---|
300 | if (totalValue <= minValue) { |
---|
301 | optimalSplitLength = offset + format.channels; |
---|
302 | minValue = totalValue; |
---|
303 | } |
---|
304 | |
---|
305 | } |
---|
306 | |
---|
307 | // If packet is not split, return the supplied packet untouched |
---|
308 | if (optimalSplitLength === data.length) |
---|
309 | return [data]; |
---|
310 | |
---|
311 | // Otherwise, split the packet into two new packets according to the |
---|
312 | // calculated optimal split length |
---|
313 | return [ |
---|
314 | new SampleArray(data.buffer.slice(0, optimalSplitLength * format.bytesPerSample)), |
---|
315 | new SampleArray(data.buffer.slice(optimalSplitLength * format.bytesPerSample)) |
---|
316 | ]; |
---|
317 | |
---|
318 | }; |
---|
319 | |
---|
320 | /** |
---|
321 | * Pushes the given packet of audio data onto the playback queue. Unlike |
---|
322 | * other private functions within Guacamole.RawAudioPlayer, the type of the |
---|
323 | * ArrayBuffer packet of audio data here need not be specific to the type |
---|
324 | * of audio (as with SampleArray). The ArrayBuffer type provided by a |
---|
325 | * Guacamole.ArrayBufferReader, for example, is sufficient. Any necessary |
---|
326 | * conversions will be performed automatically internally. |
---|
327 | * |
---|
328 | * @private |
---|
329 | * @param {ArrayBuffer} data |
---|
330 | * A raw packet of audio data that should be pushed onto the audio |
---|
331 | * playback queue. |
---|
332 | */ |
---|
333 | var pushAudioPacket = function pushAudioPacket(data) { |
---|
334 | packetQueue.push(new SampleArray(data)); |
---|
335 | }; |
---|
336 | |
---|
337 | /** |
---|
338 | * Shifts off and returns a packet of audio data from the beginning of the |
---|
339 | * playback queue. The length of this audio packet is determined |
---|
340 | * dynamically according to the click-reduction algorithm implemented by |
---|
341 | * splitAudioPacket(). |
---|
342 | * |
---|
343 | * @private |
---|
344 | * @returns {SampleArray} |
---|
345 | * A packet of audio data pulled from the beginning of the playback |
---|
346 | * queue. |
---|
347 | */ |
---|
348 | var shiftAudioPacket = function shiftAudioPacket() { |
---|
349 | |
---|
350 | // Flatten data in packet queue |
---|
351 | var data = joinAudioPackets(packetQueue); |
---|
352 | if (!data) |
---|
353 | return null; |
---|
354 | |
---|
355 | // Pull an appropriate amount of data from the front of the queue |
---|
356 | packetQueue = splitAudioPacket(data); |
---|
357 | data = packetQueue.shift(); |
---|
358 | |
---|
359 | return data; |
---|
360 | |
---|
361 | }; |
---|
362 | |
---|
363 | /** |
---|
364 | * Converts the given audio packet into an AudioBuffer, ready for playback |
---|
365 | * by the Web Audio API. Unlike the raw audio packets received by this |
---|
366 | * audio player, AudioBuffers require floating point samples and are split |
---|
367 | * into isolated planes of channel-specific data. |
---|
368 | * |
---|
369 | * @private |
---|
370 | * @param {SampleArray} data |
---|
371 | * The raw audio packet that should be converted into a Web Audio API |
---|
372 | * AudioBuffer. |
---|
373 | * |
---|
374 | * @returns {AudioBuffer} |
---|
375 | * A new Web Audio API AudioBuffer containing the provided audio data, |
---|
376 | * converted to the format used by the Web Audio API. |
---|
377 | */ |
---|
378 | var toAudioBuffer = function toAudioBuffer(data) { |
---|
379 | |
---|
380 | // Calculate total number of samples |
---|
381 | var samples = data.length / format.channels; |
---|
382 | |
---|
383 | // Determine exactly when packet CAN play |
---|
384 | var packetTime = context.currentTime; |
---|
385 | if (nextPacketTime < packetTime) |
---|
386 | nextPacketTime = packetTime; |
---|
387 | |
---|
388 | // Get audio buffer for specified format |
---|
389 | var audioBuffer = context.createBuffer(format.channels, samples, format.rate); |
---|
390 | |
---|
391 | // Convert each channel |
---|
392 | for (var channel = 0; channel < format.channels; channel++) { |
---|
393 | |
---|
394 | var audioData = audioBuffer.getChannelData(channel); |
---|
395 | |
---|
396 | // Fill audio buffer with data for channel |
---|
397 | var offset = channel; |
---|
398 | for (var i = 0; i < samples; i++) { |
---|
399 | audioData[i] = data[offset] / maxSampleValue; |
---|
400 | offset += format.channels; |
---|
401 | } |
---|
402 | |
---|
403 | } |
---|
404 | |
---|
405 | return audioBuffer; |
---|
406 | |
---|
407 | }; |
---|
408 | |
---|
409 | // Defer playback of received audio packets slightly |
---|
410 | reader.ondata = function playReceivedAudio(data) { |
---|
411 | |
---|
412 | // Push received samples onto queue |
---|
413 | pushAudioPacket(new SampleArray(data)); |
---|
414 | |
---|
415 | // Shift off an arbitrary packet of audio data from the queue (this may |
---|
416 | // be different in size from the packet just pushed) |
---|
417 | var packet = shiftAudioPacket(); |
---|
418 | if (!packet) |
---|
419 | return; |
---|
420 | |
---|
421 | // Determine exactly when packet CAN play |
---|
422 | var packetTime = context.currentTime; |
---|
423 | if (nextPacketTime < packetTime) |
---|
424 | nextPacketTime = packetTime; |
---|
425 | |
---|
426 | // Set up buffer source |
---|
427 | var source = context.createBufferSource(); |
---|
428 | source.connect(context.destination); |
---|
429 | |
---|
430 | // Use noteOn() instead of start() if necessary |
---|
431 | if (!source.start) |
---|
432 | source.start = source.noteOn; |
---|
433 | |
---|
434 | // Schedule packet |
---|
435 | source.buffer = toAudioBuffer(packet); |
---|
436 | source.start(nextPacketTime); |
---|
437 | |
---|
438 | // Update timeline by duration of scheduled packet |
---|
439 | nextPacketTime += packet.length / format.channels / format.rate; |
---|
440 | |
---|
441 | }; |
---|
442 | |
---|
443 | /** @override */ |
---|
444 | this.sync = function sync() { |
---|
445 | |
---|
446 | // Calculate elapsed time since last sync |
---|
447 | var now = context.currentTime; |
---|
448 | |
---|
449 | // Reschedule future playback time such that playback latency is |
---|
450 | // bounded within a reasonable latency threshold |
---|
451 | nextPacketTime = Math.min(nextPacketTime, now + maxLatency); |
---|
452 | |
---|
453 | }; |
---|
454 | |
---|
455 | }; |
---|
456 | |
---|
457 | Guacamole.RawAudioPlayer.prototype = new Guacamole.AudioPlayer(); |
---|
458 | |
---|
459 | /** |
---|
460 | * Determines whether the given mimetype is supported by |
---|
461 | * Guacamole.RawAudioPlayer. |
---|
462 | * |
---|
463 | * @param {String} mimetype |
---|
464 | * The mimetype to check. |
---|
465 | * |
---|
466 | * @returns {Boolean} |
---|
467 | * true if the given mimetype is supported by Guacamole.RawAudioPlayer, |
---|
468 | * false otherwise. |
---|
469 | */ |
---|
470 | Guacamole.RawAudioPlayer.isSupportedType = function isSupportedType(mimetype) { |
---|
471 | |
---|
472 | // No supported types if no Web Audio API |
---|
473 | if (!Guacamole.AudioContextFactory.getAudioContext()) |
---|
474 | return false; |
---|
475 | |
---|
476 | return Guacamole.RawAudioFormat.parse(mimetype) !== null; |
---|
477 | |
---|
478 | }; |
---|
479 | |
---|
480 | /** |
---|
481 | * Returns a list of all mimetypes supported by Guacamole.RawAudioPlayer. Only |
---|
482 | * the core mimetypes themselves will be listed. Any mimetype parameters, even |
---|
483 | * required ones, will not be included in the list. For example, "audio/L8" is |
---|
484 | * a raw audio mimetype that may be supported, but it is invalid without |
---|
485 | * additional parameters. Something like "audio/L8;rate=44100" would be valid, |
---|
486 | * however (see https://tools.ietf.org/html/rfc4856). |
---|
487 | * |
---|
488 | * @returns {String[]} |
---|
489 | * A list of all mimetypes supported by Guacamole.RawAudioPlayer, excluding |
---|
490 | * any parameters. If the necessary JavaScript APIs for playing raw audio |
---|
491 | * are absent, this list will be empty. |
---|
492 | */ |
---|
493 | Guacamole.RawAudioPlayer.getSupportedTypes = function getSupportedTypes() { |
---|
494 | |
---|
495 | // No supported types if no Web Audio API |
---|
496 | if (!Guacamole.AudioContextFactory.getAudioContext()) |
---|
497 | return []; |
---|
498 | |
---|
499 | // We support 8-bit and 16-bit raw PCM |
---|
500 | return [ |
---|
501 | 'audio/L8', |
---|
502 | 'audio/L16' |
---|
503 | ]; |
---|
504 | |
---|
505 | }; |
---|