source: OpenRLabs-Git/web2py/applications/rlabs/static/js/guacamole-common-js/modules/AudioPlayer.js

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100644
File size: 17.2 KB
Line 
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
20var 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 */
30Guacamole.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 */
56Guacamole.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 */
75Guacamole.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 */
97Guacamole.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 */
123Guacamole.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
457Guacamole.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 */
470Guacamole.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 */
493Guacamole.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};
Note: See TracBrowser for help on using the repository browser.