1 /* 2 * Copyright (C) 2013 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.webkitAudioContext) { 74 Guacamole.AudioChannel.context = new webkitAudioContext(); 75 } 76 77 /** 78 * Returns a base timestamp which can be used for scheduling future audio 79 * playback. Scheduling playback for the value returned by this function plus 80 * N will cause the associated audio to be played back N milliseconds after 81 * the function is called. 82 * 83 * @return {Number} An arbitrary channel-relative timestamp, in milliseconds. 84 */ 85 Guacamole.AudioChannel.getTimestamp = function() { 86 87 // If we have an audio context, use its timestamp 88 if (Guacamole.AudioChannel.context) 89 return Guacamole.AudioChannel.context.currentTime * 1000; 90 91 // If we have high-resolution timers, use those 92 if (window.performance) { 93 94 if (window.performance.now) 95 return window.performance.now(); 96 97 if (window.performance.webkitNow) 98 return window.performance.webkitNow(); 99 100 } 101 102 // Fallback to millisecond-resolution system time 103 return new Date().getTime(); 104 105 }; 106 107 /** 108 * Abstract representation of an audio packet. 109 * 110 * @constructor 111 * 112 * @param {String} mimetype The mimetype of the data contained by this packet. 113 * @param {Blob} data The blob of sound data contained by this packet. 114 */ 115 Guacamole.AudioChannel.Packet = function(mimetype, data) { 116 117 /** 118 * Schedules this packet for playback at the given time. 119 * 120 * @function 121 * @param {Number} when The time this packet should be played, in 122 * milliseconds. 123 */ 124 this.play = function(when) { /* NOP */ }; // Defined conditionally depending on support 125 126 // If audio API available, use it. 127 if (Guacamole.AudioChannel.context) { 128 129 var readyBuffer = null; 130 131 // By default, when decoding finishes, store buffer for future 132 // playback 133 var handleReady = function(buffer) { 134 readyBuffer = buffer; 135 }; 136 137 // Read data and start decoding 138 var reader = new FileReader(); 139 reader.onload = function() { 140 Guacamole.AudioChannel.context.decodeAudioData( 141 reader.result, 142 function(buffer) { handleReady(buffer); } 143 ); 144 }; 145 reader.readAsArrayBuffer(data); 146 147 // Set up buffer source 148 var source = Guacamole.AudioChannel.context.createBufferSource(); 149 source.connect(Guacamole.AudioChannel.context.destination); 150 151 // Use noteOn() instead of start() if necessary 152 if (!source.start) 153 source.start = source.noteOn; 154 155 var play_when; 156 157 function playDelayed(buffer) { 158 source.buffer = buffer; 159 source.start(play_when / 1000); 160 } 161 162 /** @ignore */ 163 this.play = function(when) { 164 165 play_when = when; 166 167 // If buffer available, play it NOW 168 if (readyBuffer) 169 playDelayed(readyBuffer); 170 171 // Otherwise, play when decoded 172 else 173 handleReady = playDelayed; 174 175 }; 176 177 } 178 179 else { 180 181 var play_on_load = false; 182 183 // Create audio element to house and play the data 184 var audio = null; 185 try { audio = new Audio(); } 186 catch (e) {} 187 188 if (audio) { 189 190 // Read data and start decoding 191 var reader = new FileReader(); 192 reader.onload = function() { 193 194 var binary = ""; 195 var bytes = new Uint8Array(reader.result); 196 197 // Produce binary string from bytes in buffer 198 for (var i=0; i<bytes.byteLength; i++) 199 binary += String.fromCharCode(bytes[i]); 200 201 // Convert to data URI 202 audio.src = "data:" + mimetype + ";base64," + window.btoa(binary); 203 204 // Play if play was attempted but packet wasn't loaded yet 205 if (play_on_load) 206 audio.play(); 207 208 }; 209 reader.readAsArrayBuffer(data); 210 211 function play() { 212 213 // If audio data is ready, play now 214 if (audio.src) 215 audio.play(); 216 217 // Otherwise, play when loaded 218 else 219 play_on_load = true; 220 221 } 222 223 /** @ignore */ 224 this.play = function(when) { 225 226 // Calculate time until play 227 var now = Guacamole.AudioChannel.getTimestamp(); 228 var delay = when - now; 229 230 // Play now if too late 231 if (delay < 0) 232 play(); 233 234 // Otherwise, schedule later playback 235 else 236 window.setTimeout(play, delay); 237 238 }; 239 240 } 241 242 } 243 244 }; 245