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