1 /*
  2  * Copyright (C) 2015 Glyptodon LLC
  3  *
  4  * Permission is hereby granted, free of charge, to any person obtaining a copy
  5  * of this software and associated documentation files (the "Software"), to deal
  6  * in the Software without restriction, including without limitation the rights
  7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8  * copies of the Software, and to permit persons to whom the Software is
  9  * furnished to do so, subject to the following conditions:
 10  *
 11  * The above copyright notice and this permission notice shall be included in
 12  * all copies or substantial portions of the Software.
 13  *
 14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 20  * THE SOFTWARE.
 21  */
 22 
 23 var Guacamole = Guacamole || {};
 24 
 25 /**
 26  * Abstract audio channel which queues and plays arbitrary audio data.
 27  * @constructor
 28  */
 29 Guacamole.AudioChannel = function() {
 30 
 31     /**
 32      * Reference to this AudioChannel.
 33      * @private
 34      */
 35     var channel = this;
 36 
 37     /**
 38      * When the next packet should play.
 39      * @private
 40      */
 41     var next_packet_time = 0;
 42 
 43     /**
 44      * Queues up the given data for playing by this channel once all previously
 45      * queued data has been played. If no data has been queued, the data will
 46      * play immediately.
 47      * 
 48      * @param {String} mimetype The mimetype of the data provided.
 49      * @param {Number} duration The duration of the data provided, in
 50      *                          milliseconds.
 51      * @param {Blob} data The blob data to play.
 52      */
 53     this.play = function(mimetype, duration, data) {
 54 
 55         var packet =
 56             new Guacamole.AudioChannel.Packet(mimetype, data);
 57 
 58         var now = Guacamole.AudioChannel.getTimestamp();
 59 
 60         // If underflow is detected, reschedule new packets relative to now.
 61         if (next_packet_time < now)
 62             next_packet_time = now;
 63 
 64         // Schedule next packet
 65         packet.play(next_packet_time);
 66         next_packet_time += duration;
 67 
 68     };
 69 
 70 };
 71 
 72 // Define context if available
 73 if (window.AudioContext) {
 74     Guacamole.AudioChannel.context = new AudioContext();
 75 }
 76 
 77 // Fallback to Webkit-specific AudioContext implementation
 78 else if (window.webkitAudioContext) {
 79     Guacamole.AudioChannel.context = new webkitAudioContext();
 80 }
 81 
 82 /**
 83  * Returns a base timestamp which can be used for scheduling future audio
 84  * playback. Scheduling playback for the value returned by this function plus
 85  * N will cause the associated audio to be played back N milliseconds after
 86  * the function is called.
 87  *
 88  * @return {Number} An arbitrary channel-relative timestamp, in milliseconds.
 89  */
 90 Guacamole.AudioChannel.getTimestamp = function() {
 91 
 92     // If we have an audio context, use its timestamp
 93     if (Guacamole.AudioChannel.context)
 94         return Guacamole.AudioChannel.context.currentTime * 1000;
 95 
 96     // If we have high-resolution timers, use those
 97     if (window.performance) {
 98 
 99         if (window.performance.now)
100             return window.performance.now();
101 
102         if (window.performance.webkitNow)
103             return window.performance.webkitNow();
104         
105     }
106 
107     // Fallback to millisecond-resolution system time
108     return new Date().getTime();
109 
110 };
111 
112 /**
113  * Abstract representation of an audio packet.
114  * 
115  * @constructor
116  * 
117  * @param {String} mimetype The mimetype of the data contained by this packet.
118  * @param {Blob} data The blob of sound data contained by this packet.
119  */
120 Guacamole.AudioChannel.Packet = function(mimetype, data) {
121 
122     /**
123      * Schedules this packet for playback at the given time.
124      *
125      * @function
126      * @param {Number} when The time this packet should be played, in
127      *                      milliseconds.
128      */
129     this.play = function(when) { /* NOP */ }; // Defined conditionally depending on support
130 
131     // If audio API available, use it.
132     if (Guacamole.AudioChannel.context) {
133 
134         var readyBuffer = null;
135 
136         // By default, when decoding finishes, store buffer for future
137         // playback
138         var handleReady = function(buffer) {
139             readyBuffer = buffer;
140         };
141 
142         // Read data and start decoding
143         var reader = new FileReader();
144         reader.onload = function() {
145             Guacamole.AudioChannel.context.decodeAudioData(
146                 reader.result,
147                 function(buffer) { handleReady(buffer); }
148             );
149         };
150         reader.readAsArrayBuffer(data);
151 
152         // Set up buffer source
153         var source = Guacamole.AudioChannel.context.createBufferSource();
154         source.connect(Guacamole.AudioChannel.context.destination);
155 
156         // Use noteOn() instead of start() if necessary
157         if (!source.start)
158             source.start = source.noteOn;
159 
160         var play_when;
161 
162         function playDelayed(buffer) {
163             source.buffer = buffer;
164             source.start(play_when / 1000);
165         }
166 
167         /** @ignore */
168         this.play = function(when) {
169             
170             play_when = when;
171             
172             // If buffer available, play it NOW
173             if (readyBuffer)
174                 playDelayed(readyBuffer);
175 
176             // Otherwise, play when decoded
177             else
178                 handleReady = playDelayed;
179 
180         };
181 
182     }
183 
184     else {
185 
186         var play_on_load = false;
187 
188         // Create audio element to house and play the data
189         var audio = null;
190         try { audio = new Audio(); }
191         catch (e) {}
192 
193         if (audio) {
194 
195             // Read data and start decoding
196             var reader = new FileReader();
197             reader.onload = function() {
198 
199                 var binary = "";
200                 var bytes = new Uint8Array(reader.result);
201 
202                 // Produce binary string from bytes in buffer
203                 for (var i=0; i<bytes.byteLength; i++)
204                     binary += String.fromCharCode(bytes[i]);
205 
206                 // Convert to data URI 
207                 audio.src = "data:" + mimetype + ";base64," + window.btoa(binary);
208 
209                 // Play if play was attempted but packet wasn't loaded yet
210                 if (play_on_load)
211                     audio.play();
212 
213             };
214             reader.readAsArrayBuffer(data);
215        
216             function play() {
217 
218                 // If audio data is ready, play now
219                 if (audio.src)
220                     audio.play();
221 
222                 // Otherwise, play when loaded
223                 else
224                     play_on_load = true;
225 
226             }
227             
228             /** @ignore */
229             this.play = function(when) {
230                 
231                 // Calculate time until play
232                 var now = Guacamole.AudioChannel.getTimestamp();
233                 var delay = when - now;
234                 
235                 // Play now if too late
236                 if (delay < 0)
237                     play();
238 
239                 // Otherwise, schedule later playback
240                 else
241                     window.setTimeout(play, delay);
242 
243             };
244 
245         }
246 
247     }
248 
249 };
250