1(function(){ 2 3 var inst; 4 5 /** 6 * AudioLandscape is a layer that appears as part of the parallax when the 7 * play music icon is clicked. 8 * 9 * The audio is realtime processed by JavaScript to produce a visulaisation 10 * of the audio, using the AudioContext API. 11 * 12 * @namespace audioLandscape 13 * @author Phil Glanville 14 */ 15 var audioLandscape = { 16 /** 17 * Create an instance of `audioLandscape` 18 * 19 * @memberof! audioLandscape 20 */ 21 create: function(options){ 22 if ( !audioLandscape.AudioContext ) { 23 audioLandscape.AudioContext 24 = window.AudioContext || 25 window.webkitAudioContext; 26 } 27 return audioLandscape.AudioContext && 28 this.prep.apply(Object.create(this), arguments); 29 }, 30 /** 31 * Bind methods to this instance, or a passed in context 32 * 33 * @memberof! audioLandscape 34 */ 35 bind: function(method, context){ 36 var self = this; 37 return function(){ 38 return method.apply(context||self, arguments); 39 }; 40 }, 41 /** 42 * Internal function to prep a new instance of `audioLandscape` 43 * 44 * @memberof! audioLandscape 45 * @private 46 */ 47 prep: function(options){ 48 this.options = options; 49 this.console = window.console; 50 this.canvas = jQuery(options.canvas); 51 this.window = jQuery(window); 52 this.ctx = this.canvas.get(0).getContext("2d"); 53 this.gradient = this.ctx.createLinearGradient(0, 0, 0, 300); 54 this.gradient.addColorStop(1,'#ffffff'); 55 this.gradient.addColorStop(0,'#ffffff'); 56 /// if we have optional beats json, load it now 57 if ( options.beats ) { 58 this.loadBeats(options.beats, function(beats){ 59 this.beats = beats; 60 this.loadAudio(this.options.audio); 61 }); 62 } 63 else { 64 this.loadAudio(options.audio); 65 } 66 return this; 67 }, 68 /** 69 * Event handler collection 70 * 71 * @memberof! audioLandscape 72 * @namespace audioLandscape.events 73 */ 74 events: { 75 /** 76 * Triggered on each processing of audio 77 * 78 * @memberof! audioLandscape.events 79 */ 80 audioprocess: function() { 81 // get the average for the first channel 82 var array = new Uint8Array(this.analyser.frequencyBinCount); 83 this.analyser.getByteFrequencyData(array); 84 // clear the current state 85 this.ctx.clearRect(0, 0, 1000, 325); 86 // set the fill style 87 this.ctx.fillStyle = this.gradient; 88 this.draw(array); 89 }, 90 /** 91 * Triggered when the audio first loads 92 * 93 * @memberof! audioLandscape.events 94 */ 95 audioload: function(){ 96 this.context.decodeAudioData( 97 this.request.response, 98 this.bind(this.events.audiodecoded), 99 this.bind(this.events.audioerror) 100 ); 101 }, 102 /** 103 * Triggered when a buffered part of the audio has been decoded 104 * 105 * @memberof! audioLandscape.events 106 */ 107 audiodecoded: function( buffer ){ 108 this.playBuffer(buffer); 109 }, 110 /** 111 * Triggered when an error occurs 112 * 113 * @memberof! audioLandscape.events 114 */ 115 audioerror: function( e ){ 116 this.console.log(e); 117 } 118 }, 119 /** 120 * "Beats" is work-in-progress. The idea is to have a JSON file twinned with each 121 * audio file. This JSON tracks key beats in the music for when events should occur. 122 * This would then be tied into the UI, by way of a visual demo or something. 123 * 124 * My plan is that the website would be "navigated" by the music. 125 * 126 * @memberof! audioLandscape 127 */ 128 loadBeats: function( url, callback ){ 129 jQuery.getJSON(url, jQuery.proxy(function(data){ 130 var beats = data; 131 var first = beats.list[0]; 132 beats.watchForOffset = first.offset; 133 beats.index = 0; 134 callback.call(this, beats); 135 }, this)); 136 return this; 137 }, 138 /** 139 * load a particular file by URL 140 * 141 * @memberof! audioLandscape 142 */ 143 loadAudio: function( url ){ 144 /// create a new audio context 145 this.context = new audioLandscape.AudioContext(); 146 this.javascriptNode = this.context.createScriptProcessor(2048, 1, 1); 147 this.javascriptNode.connect(this.context.destination); 148 /// setup a analyzer 149 this.analyser = this.context.createAnalyser(); 150 this.analyser.smoothingTimeConstant = 0.3; 151 this.analyser.fftSize = 512; 152 /// create a buffer source node 153 this.sourceNode = this.context.createBufferSource(); 154 this.sourceNode.connect(this.analyser); 155 this.analyser.connect(this.javascriptNode); 156 this.sourceNode.connect(this.context.destination); 157 /// listen out for the changes 158 this.javascriptNode.onaudioprocess = this.bind(this.events.audioprocess); 159 /// 160 this.request = new XMLHttpRequest(); 161 this.request.open('GET', url, true); 162 this.request.responseType = 'arraybuffer'; 163 this.request.onload = this.bind(this.events.audioload); 164 this.request.send(); 165 }, 166 /** 167 * Player a buffer of audio, at a particular offset 168 * 169 * @memberof! audioLandscape 170 */ 171 playBuffer: function( buffer, offset ){ 172 this.sourceNode.buffer = buffer; 173 this.sourceNode.start(0, offset || this.playFrom || 0); 174 }, 175 /** 176 * Stop the current audio playing 177 * 178 * @memberof! audioLandscape 179 */ 180 stop: function(){ 181 delete this.pauseAt; 182 delete this.playFrom; 183 this.sourceNode.stop(); 184 }, 185 /** 186 * Pause the current audio playing 187 * 188 * @memberof! audioLandscape 189 */ 190 pause: function(){ 191 if ( this.context && this.sourceNode ) { 192 this.pausedAt = this.context.currentTime; 193 this.sourceNode.stop(); 194 } 195 }, 196 /** 197 * Start again after a pause 198 * 199 * @memberof! audioLandscape 200 */ 201 play: function(){ 202 if ( this.pausedAt ) { 203 this.playFrom = this.pausedAt; 204 this.loadAudio( this.options.audio ); 205 delete this.pausedAt; 206 } 207 }, 208 /** 209 * Tied into the "Beats" idea. Each time the dispaly is redrawn, we check 210 * to see if the current played audio has reached the next beat time offset. 211 * This is not the ideal approach, as things would break down if the 212 * audio jumps ahead for any reason... hence why "Beats" is still a work in progress. 213 * 214 * @memberof! audioLandscape 215 * @see audioLandscape.loadBeats 216 */ 217 triggerBeats: function( sum ){ 218 if ( this.beats.watchForOffset ) { 219 if ( this.context.currentTime >= this.beats.watchForOffset ) { 220 var index = this.beats.index, 221 item = this.beats.list[index], 222 next = this.beats.list[index+1] 223 ; 224 this.beats.index++; 225 this.beats.watchForOffset = next ? next.offset : false;
226 try { 227 this.window.triggerHandler('audiolandscapebeat', { 228 offset: item.offset, 229 action: item.action, 230 power: item.power, 231 sum: sum, 232 next: next 233 }); 234 } 235 catch (ex) { 236 console.warn(ex); 237 } 238 } 239 } 240 }, 241 242 /** 243 * Redraw the currently calculated visulisation data (in terms of VU bars) 244 * 245 * @memberof! audioLandscape 246 */ 247 draw: function( array ){ 248 if ( this.context ) { 249 var sum = 0, ctx = this.ctx, i = 0, l = array.length; 250 for ( i; i<l; i++ ){ 251 sum += array[i]; 252 ctx.fillRect(i*5, 325-array[i], 3, 325); 253 } 254 this.beats && this.triggerBeats(sum); 255 this.lastSum = sum; 256 } 257 } 258 }; 259 260 var tracks = [ 261 '/audio/wind-mark-diangelo-long.ogg', 262 '/audio/gone-ft-mona-moua.ogg', 263 '/audio/tove-lo-habits-hippie-sabotage-mono.ogg' /*, 264 '/audio/a-perfect-circle_weak-and-powerless.mp3', 265 '/audio/a-perfect-cirlce_3-libras.mp3', 266 '/audio/brothomstates_adozenaday.mp3', 267 '/audio/bush_swallowed.mp3', 268 '/audio/gone-ft-mona-moua.ogg', 269 '/audio/john-tejada_young.mp3', 270 '/audio/jurassic-5_high-fidelity.mp3', 271 '/audio/telefon-tel-aviv_sound-in-a-dark-room.mp3'*/ 272 ]; 273 274 /// || tracks[Math.floor(Math.random() * tracks.length)] 275 276 0 && tabVisibilityChange && tabVisibilityChange.on('change', function(e, tab){ 277 if ( tab.hidden ) { 278 inst && inst.pause(); 279 } 280 else { 281 inst && inst.play(); 282 } 283 }); 284 285 jQuery(window) 286 .on('click', function(){ 287 if ( inst && inst.context ) { 288 console.log(Math.round(inst.context.currentTime * 100)/100, inst.lastSum); 289 } 290 }) 291 .on('audiolandscapebeat', function(e, beat){ 292 if ( beat && beat.action ) { 293 var type = beat.action.type.split('.'), subtype = type[1]; 294 switch ( type[0] ) { 295 case 'site': 296 if ( site[subtype] ) { 297 site[subtype].apply(site, beat.action.args || []); 298 } 299 break; 300 } 301 } 302 }) 303 .on('audiolandscapestart', function(){ 304 inst = audioLandscape.create({ 305 canvas: '#audio-canvas', 306 audio: tracks[1], 307 beats: '/audio/tove-lo-habits.beats.json' 308 }); 309 }) 310 .on('audiolandscapestop', function(){ 311 inst && inst.stop(); 312 }) 313 ; 314 /* 315 setTimeout(function(){ 316 site.window.triggerHandler('randomtrack'); 317 }, site.html.is('.pan-down') ? 10000 : 5000); 318 */ 319 320})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.