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 * 28 * @constructor 29 */ 30 Guacamole.AudioChannel = function AudioChannel() { 31 32 /** 33 * Reference to this AudioChannel. 34 * 35 * @private 36 * @type Guacamole.AudioChannel 37 */ 38 var channel = this; 39 40 /** 41 * The earliest possible time that the next packet could play without 42 * overlapping an already-playing packet, in milliseconds. 43 * 44 * @private 45 * @type Number 46 */ 47 var nextPacketTime = Guacamole.AudioChannel.getTimestamp(); 48 49 /** 50 * The last time that sync() was called, in milliseconds. If sync() has 51 * never been called, this will be the time the Guacamole.AudioChannel 52 * was created. 53 * 54 * @type Number 55 */ 56 var lastSync = nextPacketTime; 57 58 /** 59 * Notifies this Guacamole.AudioChannel that all audio up to the current 60 * point in time has been given via play(), and that any difference in time 61 * between queued audio packets and the current time can be considered 62 * latency. 63 */ 64 this.sync = function sync() { 65 66 // Calculate elapsed time since last sync 67 var now = Guacamole.AudioChannel.getTimestamp(); 68 var elapsed = now - lastSync; 69 70 // Reschedule future playback time such that playback latency is 71 // bounded within the duration of the last audio frame 72 nextPacketTime = Math.min(nextPacketTime, now + elapsed); 73 74 // Record sync time 75 lastSync = now; 76 77 }; 78 79 /** 80 * Queues up the given data for playing by this channel once all previously 81 * queued data has been played. If no data has been queued, the data will 82 * play immediately. 83 * 84 * @param {String} mimetype 85 * The mimetype of the audio data provided. 86 * 87 * @param {Number} duration 88 * The duration of the data provided, in milliseconds. 89 * 90 * @param {Blob} data 91 * The blob of audio data to play. 92 */ 93 this.play = function play(mimetype, duration, data) { 94 95 var packet = new Guacamole.AudioChannel.Packet(mimetype, data); 96 97 // Determine exactly when packet CAN play 98 var packetTime = Guacamole.AudioChannel.getTimestamp(); 99 if (nextPacketTime < packetTime) 100 nextPacketTime = packetTime; 101 102 // Schedule packet 103 packet.play(nextPacketTime); 104 105 // Update timeline 106 nextPacketTime += duration; 107 108 }; 109 110 }; 111 112 // Define context if available 113 if (window.AudioContext) { 114 try {Guacamole.AudioChannel.context = new AudioContext();} 115 catch (e){} 116 } 117 118 // Fallback to Webkit-specific AudioContext implementation 119 else if (window.webkitAudioContext) { 120 try {Guacamole.AudioChannel.context = new webkitAudioContext();} 121 catch (e){} 122 } 123 124 /** 125 * Returns a base timestamp which can be used for scheduling future audio 126 * playback. Scheduling playback for the value returned by this function plus 127 * N will cause the associated audio to be played back N milliseconds after 128 * the function is called. 129 * 130 * @return {Number} An arbitrary channel-relative timestamp, in milliseconds. 131 */ 132 Guacamole.AudioChannel.getTimestamp = function() { 133 134 // If we have an audio context, use its timestamp 135 if (Guacamole.AudioChannel.context) 136 return Guacamole.AudioChannel.context.currentTime * 1000; 137 138 // If we have high-resolution timers, use those 139 if (window.performance) { 140 141 if (window.performance.now) 142 return window.performance.now(); 143 144 if (window.performance.webkitNow) 145 return window.performance.webkitNow(); 146 147 } 148 149 // Fallback to millisecond-resolution system time 150 return new Date().getTime(); 151 152 }; 153 154 /** 155 * Abstract representation of an audio packet. 156 * 157 * @constructor 158 * 159 * @param {String} mimetype The mimetype of the data contained by this packet. 160 * @param {Blob} data The blob of sound data contained by this packet. 161 */ 162 Guacamole.AudioChannel.Packet = function(mimetype, data) { 163 164 /** 165 * Schedules this packet for playback at the given time. 166 * 167 * @function 168 * @param {Number} when The time this packet should be played, in 169 * milliseconds. 170 */ 171 this.play = function(when) { /* NOP */ }; // Defined conditionally depending on support 172 173 // If audio API available, use it. 174 if (Guacamole.AudioChannel.context) { 175 176 var readyBuffer = null; 177 178 // By default, when decoding finishes, store buffer for future 179 // playback 180 var handleReady = function(buffer) { 181 readyBuffer = buffer; 182 }; 183 184 // Read data and start decoding 185 var reader = new FileReader(); 186 reader.onload = function() { 187 Guacamole.AudioChannel.context.decodeAudioData( 188 reader.result, 189 function(buffer) { handleReady(buffer); } 190 ); 191 }; 192 reader.readAsArrayBuffer(data); 193 194 // Set up buffer source 195 var source = Guacamole.AudioChannel.context.createBufferSource(); 196 source.connect(Guacamole.AudioChannel.context.destination); 197 198 // Use noteOn() instead of start() if necessary 199 if (!source.start) 200 source.start = source.noteOn; 201 202 var play_when; 203 204 function playDelayed(buffer) { 205 source.buffer = buffer; 206 source.start(play_when / 1000); 207 } 208 209 /** @ignore */ 210 this.play = function(when) { 211 212 play_when = when; 213 214 // If buffer available, play it NOW 215 if (readyBuffer) 216 playDelayed(readyBuffer); 217 218 // Otherwise, play when decoded 219 else 220 handleReady = playDelayed; 221 222 }; 223 224 } 225 226 else { 227 228 var play_on_load = false; 229 230 // Create audio element to house and play the data 231 var audio = null; 232 try { audio = new Audio(); } 233 catch (e) {} 234 235 if (audio) { 236 237 // Read data and start decoding 238 var reader = new FileReader(); 239 reader.onload = function() { 240 241 var binary = ""; 242 var bytes = new Uint8Array(reader.result); 243 244 // Produce binary string from bytes in buffer 245 for (var i=0; i<bytes.byteLength; i++) 246 binary += String.fromCharCode(bytes[i]); 247 248 // Convert to data URI 249 audio.src = "data:" + mimetype + ";base64," + window.btoa(binary); 250 251 // Play if play was attempted but packet wasn't loaded yet 252 if (play_on_load) 253 audio.play(); 254 255 }; 256 reader.readAsArrayBuffer(data); 257 258 function play() { 259 260 // If audio data is ready, play now 261 if (audio.src) 262 audio.play(); 263 264 // Otherwise, play when loaded 265 else 266 play_on_load = true; 267 268 } 269 270 /** @ignore */ 271 this.play = function(when) { 272 273 // Calculate time until play 274 var now = Guacamole.AudioChannel.getTimestamp(); 275 var delay = when - now; 276 277 // Play now if too late 278 if (delay < 0) 279 play(); 280 281 // Otherwise, schedule later playback 282 else 283 window.setTimeout(play, delay); 284 285 }; 286 287 } 288 289 } 290 291 }; 292