1 
  2 /* ***** BEGIN LICENSE BLOCK *****
  3  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4  *
  5  * The contents of this file are subject to the Mozilla Public License Version
  6  * 1.1 (the "License"); you may not use this file except in compliance with
  7  * the License. You may obtain a copy of the License at
  8  * http://www.mozilla.org/MPL/
  9  *
 10  * Software distributed under the License is distributed on an "AS IS" basis,
 11  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 12  * for the specific language governing rights and limitations under the
 13  * License.
 14  *
 15  * The Original Code is guacamole-common-js.
 16  *
 17  * The Initial Developer of the Original Code is
 18  * Michael Jumper.
 19  * Portions created by the Initial Developer are Copyright (C) 2010
 20  * the Initial Developer. All Rights Reserved.
 21  *
 22  * Contributor(s):
 23  *
 24  * Alternatively, the contents of this file may be used under the terms of
 25  * either the GNU General Public License Version 2 or later (the "GPL"), or
 26  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 27  * in which case the provisions of the GPL or the LGPL are applicable instead
 28  * of those above. If you wish to allow use of your version of this file only
 29  * under the terms of either the GPL or the LGPL, and not to allow others to
 30  * use your version of this file under the terms of the MPL, indicate your
 31  * decision by deleting the provisions above and replace them with the notice
 32  * and other provisions required by the GPL or the LGPL. If you do not delete
 33  * the provisions above, a recipient may use your version of this file under
 34  * the terms of any one of the MPL, the GPL or the LGPL.
 35  *
 36  * ***** END LICENSE BLOCK ***** */
 37 
 38 /**
 39  * Namespace for all Guacamole JavaScript objects.
 40  * @namespace
 41  */
 42 var Guacamole = Guacamole || {};
 43 
 44 /**
 45  * Abstract audio channel which queues and plays arbitrary audio data.
 46  * @constructor
 47  */
 48 Guacamole.AudioChannel = function() {
 49 
 50     /**
 51      * Reference to this AudioChannel.
 52      * @private
 53      */
 54     var channel = this;
 55 
 56     /**
 57      * When the next packet should play.
 58      * @private
 59      */
 60     var next_packet_time = 0;
 61 
 62     /**
 63      * Queues up the given data for playing by this channel once all previously
 64      * queued data has been played. If no data has been queued, the data will
 65      * play immediately.
 66      * 
 67      * @param {String} mimetype The mimetype of the data provided.
 68      * @param {Number} duration The duration of the data provided, in
 69      *                          milliseconds.
 70      * @param {String} data The base64-encoded data to play.
 71      */
 72     this.play = function(mimetype, duration, data) {
 73 
 74         var packet =
 75             new Guacamole.AudioChannel.Packet(mimetype, data);
 76 
 77         var now = Guacamole.AudioChannel.getTimestamp();
 78 
 79         // If underflow is detected, reschedule new packets relative to now.
 80         if (next_packet_time < now)
 81             next_packet_time = now;
 82 
 83         // Schedule next packet
 84         packet.play(next_packet_time);
 85         next_packet_time += duration;
 86 
 87     };
 88 
 89 };
 90 
 91 // Define context if available
 92 if (window.webkitAudioContext) {
 93     Guacamole.AudioChannel.context = new webkitAudioContext();
 94 }
 95 
 96 /**
 97  * Returns a base timestamp which can be used for scheduling future audio
 98  * playback. Scheduling playback for the value returned by this function plus
 99  * N will cause the associated audio to be played back N milliseconds after
100  * the function is called.
101  *
102  * @return {Number} An arbitrary channel-relative timestamp, in milliseconds.
103  */
104 Guacamole.AudioChannel.getTimestamp = function() {
105 
106     // If we have an audio context, use its timestamp
107     if (Guacamole.AudioChannel.context)
108         return Guacamole.AudioChannel.context.currentTime * 1000;
109 
110     // If we have high-resolution timers, use those
111     if (window.performance) {
112 
113         if (window.performance.now)
114             return window.performance.now();
115 
116         if (window.performance.webkitNow)
117             return window.performance.webkitNow();
118         
119     }
120 
121     // Fallback to millisecond-resolution system time
122     return new Date().getTime();
123 
124 };
125 
126 /**
127  * Abstract representation of an audio packet.
128  * 
129  * @constructor
130  * 
131  * @param {String} mimetype The mimetype of the data contained by this packet.
132  * @param {String} data The base64-encoded sound data contained by this packet.
133  */
134 Guacamole.AudioChannel.Packet = function(mimetype, data) {
135 
136     /**
137      * Schedules this packet for playback at the given time.
138      *
139      * @function
140      * @param {Number} when The time this packet should be played, in
141      *                      milliseconds.
142      */
143     this.play = undefined; // Defined conditionally depending on support
144 
145     // If audio API available, use it.
146     if (Guacamole.AudioChannel.context) {
147 
148         var readyBuffer = null;
149 
150         // By default, when decoding finishes, store buffer for future
151         // playback
152         var handleReady = function(buffer) {
153             readyBuffer = buffer;
154         };
155 
156         // Convert to ArrayBuffer
157         var binary = window.atob(data);
158         var arrayBuffer = new ArrayBuffer(binary.length);
159         var bufferView = new Uint8Array(arrayBuffer);
160 
161         for (var i=0; i<binary.length; i++)
162             bufferView[i] = binary.charCodeAt(i);
163 
164         // Get context and start decoding
165         Guacamole.AudioChannel.context.decodeAudioData(
166             arrayBuffer,
167             function(buffer) { handleReady(buffer); }
168         );
169 
170         // Set up buffer source
171         var source = Guacamole.AudioChannel.context.createBufferSource();
172         source.connect(Guacamole.AudioChannel.context.destination);
173 
174         var play_when;
175 
176         function playDelayed(buffer) {
177             source.buffer = buffer;
178             source.noteOn(play_when / 1000);
179         }
180 
181         /** @ignore */
182         this.play = function(when) {
183             
184             play_when = when;
185             
186             // If buffer available, play it NOW
187             if (readyBuffer)
188                 playDelayed(readyBuffer);
189 
190             // Otherwise, play when decoded
191             else
192                 handleReady = playDelayed;
193 
194         };
195 
196     }
197 
198     else {
199 
200         // Build data URI
201         var data_uri = "data:" + mimetype + ";base64," + data;
202        
203         // Create audio element to house and play the data
204         var audio = new Audio();
205         audio.src = data_uri;
206       
207         /** @ignore */
208         this.play = function(when) {
209             
210             // Calculate time until play
211             var now = Guacamole.AudioChannel.getTimestamp();
212             var delay = when - now;
213             
214             // Play now if too late
215             if (delay < 0)
216                 audio.play();
217 
218             // Otherwise, schedule later playback
219             else
220                 window.setTimeout(function() {
221                     audio.play();
222                 }, delay);
223 
224         };
225 
226     }
227 
228 };
229