source: OpenRLabs-Git/web2py/applications/rlabs/static/js/guacamole-common-js/modules/AudioRecorder.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: 18.1 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 recorder which streams arbitrary audio data to an underlying
24 * Guacamole.OutputStream. It is up to implementations of this class to provide
25 * some means of handling this Guacamole.OutputStream. Data produced by the
26 * recorder is to be sent along the provided stream immediately.
27 *
28 * @constructor
29 */
30Guacamole.AudioRecorder = function AudioRecorder() {
31
32    /**
33     * Callback which is invoked when the audio recording process has stopped
34     * and the underlying Guacamole stream has been closed normally. Audio will
35     * only resume recording if a new Guacamole.AudioRecorder is started. This
36     * Guacamole.AudioRecorder instance MAY NOT be reused.
37     *
38     * @event
39     */
40    this.onclose = null;
41
42    /**
43     * Callback which is invoked when the audio recording process cannot
44     * continue due to an error, if it has started at all. The underlying
45     * Guacamole stream is automatically closed. Future attempts to record
46     * audio should not be made, and this Guacamole.AudioRecorder instance
47     * MAY NOT be reused.
48     *
49     * @event
50     */
51    this.onerror = null;
52
53};
54
55/**
56 * Determines whether the given mimetype is supported by any built-in
57 * implementation of Guacamole.AudioRecorder, and thus will be properly handled
58 * by Guacamole.AudioRecorder.getInstance().
59 *
60 * @param {String} mimetype
61 *     The mimetype to check.
62 *
63 * @returns {Boolean}
64 *     true if the given mimetype is supported by any built-in
65 *     Guacamole.AudioRecorder, false otherwise.
66 */
67Guacamole.AudioRecorder.isSupportedType = function isSupportedType(mimetype) {
68
69    return Guacamole.RawAudioRecorder.isSupportedType(mimetype);
70
71};
72
73/**
74 * Returns a list of all mimetypes supported by any built-in
75 * Guacamole.AudioRecorder, in rough order of priority. Beware that only the
76 * core mimetypes themselves will be listed. Any mimetype parameters, even
77 * required ones, will not be included in the list. For example, "audio/L8" is
78 * a supported raw audio mimetype that is supported, but it is invalid without
79 * additional parameters. Something like "audio/L8;rate=44100" would be valid,
80 * however (see https://tools.ietf.org/html/rfc4856).
81 *
82 * @returns {String[]}
83 *     A list of all mimetypes supported by any built-in
84 *     Guacamole.AudioRecorder, excluding any parameters.
85 */
86Guacamole.AudioRecorder.getSupportedTypes = function getSupportedTypes() {
87
88    return Guacamole.RawAudioRecorder.getSupportedTypes();
89
90};
91
92/**
93 * Returns an instance of Guacamole.AudioRecorder providing support for the
94 * given audio format. If support for the given audio format is not available,
95 * null is returned.
96 *
97 * @param {Guacamole.OutputStream} stream
98 *     The Guacamole.OutputStream to send audio data through.
99 *
100 * @param {String} mimetype
101 *     The mimetype of the audio data to be sent along the provided stream.
102 *
103 * @return {Guacamole.AudioRecorder}
104 *     A Guacamole.AudioRecorder instance supporting the given mimetype and
105 *     writing to the given stream, or null if support for the given mimetype
106 *     is absent.
107 */
108Guacamole.AudioRecorder.getInstance = function getInstance(stream, mimetype) {
109
110    // Use raw audio recorder if possible
111    if (Guacamole.RawAudioRecorder.isSupportedType(mimetype))
112        return new Guacamole.RawAudioRecorder(stream, mimetype);
113
114    // No support for given mimetype
115    return null;
116
117};
118
119/**
120 * Implementation of Guacamole.AudioRecorder providing support for raw PCM
121 * format audio. This recorder relies only on the Web Audio API and does not
122 * require any browser-level support for its audio formats.
123 *
124 * @constructor
125 * @augments Guacamole.AudioRecorder
126 * @param {Guacamole.OutputStream} stream
127 *     The Guacamole.OutputStream to write audio data to.
128 *
129 * @param {String} mimetype
130 *     The mimetype of the audio data to send along the provided stream, which
131 *     must be a "audio/L8" or "audio/L16" mimetype with necessary parameters,
132 *     such as: "audio/L16;rate=44100,channels=2".
133 */
134Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) {
135
136    /**
137     * Reference to this RawAudioRecorder.
138     *
139     * @private
140     * @type {Guacamole.RawAudioRecorder}
141     */
142    var recorder = this;
143
144    /**
145     * The size of audio buffer to request from the Web Audio API when
146     * recording or processing audio, in sample-frames. This must be a power of
147     * two between 256 and 16384 inclusive, as required by
148     * AudioContext.createScriptProcessor().
149     *
150     * @private
151     * @constant
152     * @type {Number}
153     */
154    var BUFFER_SIZE = 2048;
155
156    /**
157     * The window size to use when applying Lanczos interpolation, commonly
158     * denoted by the variable "a".
159     * See: https://en.wikipedia.org/wiki/Lanczos_resampling
160     *
161     * @private
162     * @contant
163     * @type Number
164     */
165    var LANCZOS_WINDOW_SIZE = 3;
166
167    /**
168     * The format of audio this recorder will encode.
169     *
170     * @private
171     * @type {Guacamole.RawAudioFormat}
172     */
173    var format = Guacamole.RawAudioFormat.parse(mimetype);
174
175    /**
176     * An instance of a Web Audio API AudioContext object, or null if the
177     * Web Audio API is not supported.
178     *
179     * @private
180     * @type {AudioContext}
181     */
182    var context = Guacamole.AudioContextFactory.getAudioContext();
183
184    /**
185     * A function which directly invokes the browser's implementation of
186     * navigator.getUserMedia() with all provided parameters.
187     *
188     * @type Function
189     */
190    var getUserMedia = (navigator.getUserMedia
191            || navigator.webkitGetUserMedia
192            || navigator.mozGetUserMedia
193            || navigator.msGetUserMedia).bind(navigator);
194
195    /**
196     * Guacamole.ArrayBufferWriter wrapped around the audio output stream
197     * provided when this Guacamole.RawAudioRecorder was created.
198     *
199     * @private
200     * @type {Guacamole.ArrayBufferWriter}
201     */
202    var writer = new Guacamole.ArrayBufferWriter(stream);
203
204    /**
205     * The type of typed array that will be used to represent each audio packet
206     * internally. This will be either Int8Array or Int16Array, depending on
207     * whether the raw audio format is 8-bit or 16-bit.
208     *
209     * @private
210     * @constructor
211     */
212    var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array;
213
214    /**
215     * The maximum absolute value of any sample within a raw audio packet sent
216     * by this audio recorder. This depends only on the size of each sample,
217     * and will be 128 for 8-bit audio and 32768 for 16-bit audio.
218     *
219     * @private
220     * @type {Number}
221     */
222    var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768;
223
224    /**
225     * The total number of audio samples read from the local audio input device
226     * over the life of this audio recorder.
227     *
228     * @private
229     * @type {Number}
230     */
231    var readSamples = 0;
232
233    /**
234     * The total number of audio samples written to the underlying Guacamole
235     * connection over the life of this audio recorder.
236     *
237     * @private
238     * @type {Number}
239     */
240    var writtenSamples = 0;
241
242    /**
243     * The audio stream provided by the browser, if allowed. If no stream has
244     * yet been received, this will be null.
245     *
246     * @type MediaStream
247     */
248    var mediaStream = null;
249
250    /**
251     * The source node providing access to the local audio input device.
252     *
253     * @private
254     * @type {MediaStreamAudioSourceNode}
255     */
256    var source = null;
257
258    /**
259     * The script processing node which receives audio input from the media
260     * stream source node as individual audio buffers.
261     *
262     * @private
263     * @type {ScriptProcessorNode}
264     */
265    var processor = null;
266
267    /**
268     * The normalized sinc function. The normalized sinc function is defined as
269     * 1 for x=0 and sin(PI * x) / (PI * x) for all other values of x.
270     *
271     * See: https://en.wikipedia.org/wiki/Sinc_function
272     *
273     * @private
274     * @param {Number} x
275     *     The point at which the normalized sinc function should be computed.
276     *
277     * @returns {Number}
278     *     The value of the normalized sinc function at x.
279     */
280    var sinc = function sinc(x) {
281
282        // The value of sinc(0) is defined as 1
283        if (x === 0)
284            return 1;
285
286        // Otherwise, normlized sinc(x) is sin(PI * x) / (PI * x)
287        var piX = Math.PI * x;
288        return Math.sin(piX) / piX;
289
290    };
291
292    /**
293     * Calculates the value of the Lanczos kernal at point x for a given window
294     * size. See: https://en.wikipedia.org/wiki/Lanczos_resampling
295     *
296     * @private
297     * @param {Number} x
298     *     The point at which the value of the Lanczos kernel should be
299     *     computed.
300     *
301     * @param {Number} a
302     *     The window size to use for the Lanczos kernel.
303     *
304     * @returns {Number}
305     *     The value of the Lanczos kernel at the given point for the given
306     *     window size.
307     */
308    var lanczos = function lanczos(x, a) {
309
310        // Lanczos is sinc(x) * sinc(x / a) for -a < x < a ...
311        if (-a < x && x < a)
312            return sinc(x) * sinc(x / a);
313
314        // ... and 0 otherwise
315        return 0;
316
317    };
318
319    /**
320     * Determines the value of the waveform represented by the audio data at
321     * the given location. If the value cannot be determined exactly as it does
322     * not correspond to an exact sample within the audio data, the value will
323     * be derived through interpolating nearby samples.
324     *
325     * @private
326     * @param {Float32Array} audioData
327     *     An array of audio data, as returned by AudioBuffer.getChannelData().
328     *
329     * @param {Number} t
330     *     The relative location within the waveform from which the value
331     *     should be retrieved, represented as a floating point number between
332     *     0 and 1 inclusive, where 0 represents the earliest point in time and
333     *     1 represents the latest.
334     *
335     * @returns {Number}
336     *     The value of the waveform at the given location.
337     */
338    var interpolateSample = function getValueAt(audioData, t) {
339
340        // Convert [0, 1] range to [0, audioData.length - 1]
341        var index = (audioData.length - 1) * t;
342
343        // Determine the start and end points for the summation used by the
344        // Lanczos interpolation algorithm (see: https://en.wikipedia.org/wiki/Lanczos_resampling)
345        var start = Math.floor(index) - LANCZOS_WINDOW_SIZE + 1;
346        var end = Math.floor(index) + LANCZOS_WINDOW_SIZE;
347
348        // Calculate the value of the Lanczos interpolation function for the
349        // required range
350        var sum = 0;
351        for (var i = start; i <= end; i++) {
352            sum += (audioData[i] || 0) * lanczos(index - i, LANCZOS_WINDOW_SIZE);
353        }
354
355        return sum;
356
357    };
358
359    /**
360     * Converts the given AudioBuffer into an audio packet, ready for streaming
361     * along the underlying output stream. Unlike the raw audio packets used by
362     * this audio recorder, AudioBuffers require floating point samples and are
363     * split into isolated planes of channel-specific data.
364     *
365     * @private
366     * @param {AudioBuffer} audioBuffer
367     *     The Web Audio API AudioBuffer that should be converted to a raw
368     *     audio packet.
369     *
370     * @returns {SampleArray}
371     *     A new raw audio packet containing the audio data from the provided
372     *     AudioBuffer.
373     */
374    var toSampleArray = function toSampleArray(audioBuffer) {
375
376        // Track overall amount of data read
377        var inSamples = audioBuffer.length;
378        readSamples += inSamples;
379
380        // Calculate the total number of samples that should be written as of
381        // the audio data just received and adjust the size of the output
382        // packet accordingly
383        var expectedWrittenSamples = Math.round(readSamples * format.rate / audioBuffer.sampleRate);
384        var outSamples = expectedWrittenSamples - writtenSamples;
385
386        // Update number of samples written
387        writtenSamples += outSamples;
388
389        // Get array for raw PCM storage
390        var data = new SampleArray(outSamples * format.channels);
391
392        // Convert each channel
393        for (var channel = 0; channel < format.channels; channel++) {
394
395            var audioData = audioBuffer.getChannelData(channel);
396
397            // Fill array with data from audio buffer channel
398            var offset = channel;
399            for (var i = 0; i < outSamples; i++) {
400                data[offset] = interpolateSample(audioData, i / (outSamples - 1)) * maxSampleValue;
401                offset += format.channels;
402            }
403
404        }
405
406        return data;
407
408    };
409
410    /**
411     * Requests access to the user's microphone and begins capturing audio. All
412     * received audio data is resampled as necessary and forwarded to the
413     * Guacamole stream underlying this Guacamole.RawAudioRecorder. This
414     * function must be invoked ONLY ONCE per instance of
415     * Guacamole.RawAudioRecorder.
416     *
417     * @private
418     */
419    var beginAudioCapture = function beginAudioCapture() {
420
421        // Attempt to retrieve an audio input stream from the browser
422        getUserMedia({ 'audio' : true }, function streamReceived(stream) {
423
424            // Create processing node which receives appropriately-sized audio buffers
425            processor = context.createScriptProcessor(BUFFER_SIZE, format.channels, format.channels);
426            processor.connect(context.destination);
427
428            // Send blobs when audio buffers are received
429            processor.onaudioprocess = function processAudio(e) {
430                writer.sendData(toSampleArray(e.inputBuffer).buffer);
431            };
432
433            // Connect processing node to user's audio input source
434            source = context.createMediaStreamSource(stream);
435            source.connect(processor);
436
437            // Save stream for later cleanup
438            mediaStream = stream;
439
440        }, function streamDenied() {
441
442            // Simply end stream if audio access is not allowed
443            writer.sendEnd();
444
445            // Notify of closure
446            if (recorder.onerror)
447                recorder.onerror();
448
449        });
450
451    };
452
453    /**
454     * Stops capturing audio, if the capture has started, freeing all associated
455     * resources. If the capture has not started, this function simply ends the
456     * underlying Guacamole stream.
457     *
458     * @private
459     */
460    var stopAudioCapture = function stopAudioCapture() {
461
462        // Disconnect media source node from script processor
463        if (source)
464            source.disconnect();
465
466        // Disconnect associated script processor node
467        if (processor)
468            processor.disconnect();
469
470        // Stop capture
471        if (mediaStream) {
472            var tracks = mediaStream.getTracks();
473            for (var i = 0; i < tracks.length; i++)
474                tracks[i].stop();
475        }
476
477        // Remove references to now-unneeded components
478        processor = null;
479        source = null;
480        mediaStream = null;
481
482        // End stream
483        writer.sendEnd();
484
485    };
486
487    // Once audio stream is successfully open, request and begin reading audio
488    writer.onack = function audioStreamAcknowledged(status) {
489
490        // Begin capture if successful response and not yet started
491        if (status.code === Guacamole.Status.Code.SUCCESS && !mediaStream)
492            beginAudioCapture();
493
494        // Otherwise stop capture and cease handling any further acks
495        else {
496
497            // Stop capturing audio
498            stopAudioCapture();
499            writer.onack = null;
500
501            // Notify if stream has closed normally
502            if (status.code === Guacamole.Status.Code.RESOURCE_CLOSED) {
503                if (recorder.onclose)
504                    recorder.onclose();
505            }
506
507            // Otherwise notify of closure due to error
508            else {
509                if (recorder.onerror)
510                    recorder.onerror();
511            }
512
513        }
514
515    };
516
517};
518
519Guacamole.RawAudioRecorder.prototype = new Guacamole.AudioRecorder();
520
521/**
522 * Determines whether the given mimetype is supported by
523 * Guacamole.RawAudioRecorder.
524 *
525 * @param {String} mimetype
526 *     The mimetype to check.
527 *
528 * @returns {Boolean}
529 *     true if the given mimetype is supported by Guacamole.RawAudioRecorder,
530 *     false otherwise.
531 */
532Guacamole.RawAudioRecorder.isSupportedType = function isSupportedType(mimetype) {
533
534    // No supported types if no Web Audio API
535    if (!Guacamole.AudioContextFactory.getAudioContext())
536        return false;
537
538    return Guacamole.RawAudioFormat.parse(mimetype) !== null;
539
540};
541
542/**
543 * Returns a list of all mimetypes supported by Guacamole.RawAudioRecorder. Only
544 * the core mimetypes themselves will be listed. Any mimetype parameters, even
545 * required ones, will not be included in the list. For example, "audio/L8" is
546 * a raw audio mimetype that may be supported, but it is invalid without
547 * additional parameters. Something like "audio/L8;rate=44100" would be valid,
548 * however (see https://tools.ietf.org/html/rfc4856).
549 *
550 * @returns {String[]}
551 *     A list of all mimetypes supported by Guacamole.RawAudioRecorder,
552 *     excluding any parameters. If the necessary JavaScript APIs for recording
553 *     raw audio are absent, this list will be empty.
554 */
555Guacamole.RawAudioRecorder.getSupportedTypes = function getSupportedTypes() {
556
557    // No supported types if no Web Audio API
558    if (!Guacamole.AudioContextFactory.getAudioContext())
559        return [];
560
561    // We support 8-bit and 16-bit raw PCM
562    return [
563        'audio/L8',
564        'audio/L16'
565    ];
566
567};
Note: See TracBrowser for help on using the repository browser.