├── .gitignore ├── src ├── img │ ├── git-img-1.jpg │ ├── git-img-2.jpg │ ├── git-img-3.jpg │ ├── git-img-4.jpg │ ├── git-img-5.gif │ ├── vimeo-img.jpg │ └── technical-talks.jpg ├── audio │ ├── accordian.mp3 │ └── church-bell.mp3 ├── data │ └── demo.json ├── index.html └── js │ ├── sosv.js │ ├── sosv.min.js │ └── howler.js ├── Gruntfile.js ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/img/git-img-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/HEAD/src/img/git-img-1.jpg -------------------------------------------------------------------------------- /src/img/git-img-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/HEAD/src/img/git-img-2.jpg -------------------------------------------------------------------------------- /src/img/git-img-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/HEAD/src/img/git-img-3.jpg -------------------------------------------------------------------------------- /src/img/git-img-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/HEAD/src/img/git-img-4.jpg -------------------------------------------------------------------------------- /src/img/git-img-5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/HEAD/src/img/git-img-5.gif -------------------------------------------------------------------------------- /src/img/vimeo-img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/HEAD/src/img/vimeo-img.jpg -------------------------------------------------------------------------------- /src/audio/accordian.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/HEAD/src/audio/accordian.mp3 -------------------------------------------------------------------------------- /src/audio/church-bell.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/HEAD/src/audio/church-bell.mp3 -------------------------------------------------------------------------------- /src/img/technical-talks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/HEAD/src/img/technical-talks.jpg -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(grunt) { 3 | 4 | grunt.initConfig({ 5 | 6 | pkg: grunt.file.readJSON('package.json'), 7 | 8 | uglify: { 9 | sosvjs: { 10 | files: { 11 | 'src/js/sosv.min.js': ['src/js/howler.js', 'src/js/sosv.js'] 12 | } 13 | } 14 | }, 15 | 16 | copy: { 17 | build: { 18 | files: { 19 | 'build/sosv.min.js': ['src/js/sosv.min.js'] 20 | } 21 | } 22 | }, 23 | 24 | }); 25 | 26 | grunt.loadNpmTasks('grunt-contrib-uglify'); 27 | grunt.loadNpmTasks('grunt-contrib-copy'); 28 | 29 | grunt.registerTask('build', 'General build task', ['uglify:sosvjs', 'copy:build']); 30 | }; -------------------------------------------------------------------------------- /src/data/demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "pano", 3 | 4 | "lat": "43.950649", 5 | "lng": "4.806588", 6 | "heading": "60", 7 | "pitch": "1", 8 | 9 | "sounds": [ 10 | { 11 | "name": "accordion", 12 | "lat": "43.950648973687", 13 | "lng": "4.806433016568121", 14 | "src": [ 15 | "audio/accordian.mp3" 16 | ], 17 | "db": "80", 18 | "pause": "0" 19 | }, 20 | { 21 | "name": "church-bell-2", 22 | "lat": "43.95087323161753", 23 | "lng": "4.8068590177625765", 24 | "src": [ 25 | "audio/church-bell.mp3" 26 | ], 27 | "db": "100", 28 | "pause": "0" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sounds-of-Street-View-Framework", 3 | "version": "0.0.1", 4 | "description": "none", 5 | "main": "Gruntfile.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/Amplifon/Sounds-of-Street-View-Framework.git" 9 | }, 10 | "keywords": [], 11 | "author": { 12 | "name": "Amplifon", 13 | "url": "http://www.amplifon.co.uk/sounds-of-street-view/" 14 | }, 15 | "contributors": [ 16 | { 17 | "name": "Amplifon", 18 | "email": "" 19 | } 20 | ], 21 | "client": { 22 | "name": "Amplifon", 23 | "string": "amplifon" 24 | }, 25 | "project": { 26 | "name": "Sounds Of Street View", 27 | "title": "SoundsOfStreetView", 28 | "string": "sounds-of-street-view" 29 | }, 30 | "license": "", 31 | "devDependencies": { 32 | "grunt": "~0.4.2", 33 | "grunt-contrib-uglify": "~0.2.4", 34 | "grunt-contrib-copy": "~0.5.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Amplifon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SOSV Framework 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 23 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 |

Powered by Sounds of Street View by Amplifon

50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sounds of Street View Framework 2 | 3 | ###A JavaScript library to add sound to a Google Street View experience. 4 | 5 | Watch the promotional video for an explanation and overview of what can be achieved 6 | with the Sounds of Street View Framework. 7 | 8 | [![ScreenShot](https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/master/src/img/vimeo-img.jpg)](https://vimeo.com/101599212) 9 | 10 | Explore three environments created by Amplifon on [the Sounds of Street View website](http://www.amplifon.co.uk/sounds-of-street-view/). Tell us about your Sounds of Street View project via [Email](mailto:kelly.ward@epiphanysolutions.co.uk) or [Twitter](https://twitter.com/intent/tweet?url=http%3A%2F%2Fexample.com&text=.%40amplifon%20Check%20out%20my%20Sounds%20of%20Street%20View%20experience%20here.) and you could win a trip of your choice! All entries are placed into our [Gallery Collection](http://www.amplifon.co.uk/sounds-of-street-view/gallery/index.html) too. View the [Terms and Conditions](http://www.amplifon.co.uk/about-amplifon/latest-amplifon-news/amplifon's-sounds-of-street-view-launches-with-development-competition/) for more information. 11 | 12 | ## Dependencies 13 | 14 | - [Google Maps API v3](https://developers.google.com/maps/documentation/javascript/basics) 15 | - [JQuery](http://jquery.com/) 16 | 17 | ## How to use it 18 | 19 | The following explanations are specifically written for those with only a 20 | rudimentary understanding of HTML and JavaScript. If you are a developer, or 21 | are confident that you have a decent understanding of these technologies, then 22 | head straight over to the [demo files in the src folder](https://github.com/Amplifon/Sounds-of-Street-View-Framework/tree/master/src), 23 | where everything should be fairly self explanatory. 24 | 25 | ### Include the dependencies 26 | 27 | The first thing to do is to include the 2 JavaScript dependencies within the head section 28 | of your HTML page. You need to load the Google Maps API and jQuery. 29 | 30 | ```html 31 | 32 | 33 | 34 | 35 | 36 | 37 | ``` 38 | 39 | ### Create an element on your page for the Google Street View instance 40 | 41 | Make sure when you create this HTML element that it has a unique ID, and that 42 | the ID you provide it with matches the ID that you stipulate within your JSON data 43 | file (see below) 44 | 45 | ```html 46 |
47 | ``` 48 | 49 | ### Create your JSON data file 50 | 51 | The JSON file should contain all the data for your Sounds of Street View application, including 52 | the start position of the map when the application loads, as well as the positioning and volume 53 | levels for all of your sounds. The "id" property at the start of the file should exactly match the 54 | unique ID that you gave the HTML element within your HTML (see above). 55 | 56 | This is the demo JSON file included within the src demo as part of this repo. 57 | 58 | ```json 59 | { 60 | "id": "pano", 61 | 62 | "lat": "43.950649", 63 | "lng": "4.806588", 64 | "heading": "60", 65 | "pitch": "1", 66 | 67 | "sounds": [ 68 | { 69 | "name": "accordion", 70 | "lat": "43.950648973687", 71 | "lng": "4.806433016568121", 72 | "src": [ 73 | "audio/accordian.mp3" 74 | ], 75 | "db": "80", 76 | "pause": "0" 77 | }, 78 | { 79 | "name": "church-bell-2", 80 | "lat": "43.951337460699705", 81 | "lng": "4.8069440499275515", 82 | "src": [ 83 | "audio/church-bell.mp3" 84 | ], 85 | "db": "100", 86 | "pause": "0" 87 | } 88 | 89 | ... add more sound objects here ... 90 | ] 91 | } 92 | ``` 93 | #### JSON Properties 94 | 95 | Here is a more thorough breakdown of the properties included within the JSON data file; 96 | 97 | **id** 98 | This should match the ID of your HTML element where the Google Street View will appear 99 | 100 | **lat** 101 | This is the latitude position value where your application will start upon load 102 | 103 | **lng** 104 | This is the longitude position value where your application will start upon load 105 | 106 | ![ScreenShot](https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/master/src/img/git-img-3.jpg) 107 | 108 | **heading** 109 | This is the heading position value where your application will start upon load (ie. which direction the user is facing) 110 | 111 | **pitch** 112 | This is the pitch position value where your application will start upon load (ie. how low or high the user is facing) 113 | 114 | **sounds** 115 | This is an array of sound objects (denoted by the square brackets) that will be positioned within your application. 116 | The Framework does not impose any limits upon how many sounds you can add, however be aware that any computer viewing 117 | your app *will* have limitations for how many current sounds it can track and play depending upon the processing 118 | power of the machine. 119 | 120 | #### Individual sound properties 121 | 122 | Each sound has the following properties; 123 | 124 | **name** 125 | Although the user will not see this name, it can be useful when building your application as 126 | the name of each sound object will appear on hover (as shown below) when you view your application in dev mode. 127 | You can trigger dev mode by appending ?dev=true to the end of your application URL. 128 | 129 | ![ScreenShot](https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/master/src/img/git-img-1.jpg 130 | ) 131 | 132 | **lat** 133 | This is the latitude position where your sound will be positioned on your Street View map 134 | 135 | **lng** 136 | This is the longitude position where your sound will be positioned on your Street View map 137 | 138 | **src** 139 | The path to the mp3 file associated with the sound object 140 | 141 | **db** 142 | The "loudness" of the sound, ie how far away it can be heard from. Although db suggests 143 | that this is the decibel level for a sound it is not calculated at decibel levels. The db property 144 | should be a value between 1 and 100, with 1 being very quiet and 100 being very loud. 145 | 146 | **pause** 147 | The length of time to wait between each loop of the sound. This is a length of time in 148 | milliseconds so a 1 second pause between each loop would have a pause value of 1000. 149 | 150 | **For more explanation on how the individual sound formulas come together, visit our collection of video explanations from our technical expert. https://vimeo.com/album/2977202** 151 | 152 | [![ScreenShot](https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/62c6d6a277aaf3f32b752972b358a7880f99b29a/src/img/technical-talks.jpg)](https://vimeo.com/album/2977202) 153 | 154 | #### Location Suggestions 155 | 156 | Here is a list of great street view locations to get you started, with the lat and lng values already supplied! 157 | 158 | - [Millenium Park, Chicago](https://www.google.com/maps/@41.882772,-87.622462,3a,75y,96.25h,91.53t/data=!3m5!1e1!3m3!1sPyoeKzjCqY-mHhC42fCuTw!2e0!3e5) - "lat": "41.882772", "lng": "-87.622462" 159 | - [Central Park, New York](https://www.google.com/maps/@40.774459,-73.970928,3a,75y,151.6h,81.55t/data=!3m5!1e1!3m3!1sxPoyPnFWo0QqkK2NhUqRgw!2e0!3e5) - "lat": "40.774459", "lng": "-73.970928" 160 | - [Venice](https://www.google.co.uk/maps/@45.437134,12.333657,3a,75y,162.89h,71.81t/data=!3m4!1e1!3m2!1s2SPIL-tGMy9C7lupTyJJXg!2e0) - "lat": "45.437134", "lng": "12.333657" 161 | - [Rainforest, Manaus, Brazil](https://www.google.com/maps/@-2.945071,-60.676237,3a,75y,185.64h,85.85t/data=!3m5!1e1!3m3!1sYETFM_LVtG9vvRH_NAOI-A!2e0!3e2) - "lat": "-2.945071", "lng": "-60.676237" 162 | - [Florence, Italy](https://www.google.com/maps/@43.773421,11.25517,3a,75y,252.86h,86.84t/data=!3m5!1e1!3m3!1sdGeTbsAiR5Fw6RSpbHcESw!2e0!3e5) - "lat": "43.773421", "lng": "11.25517" 163 | 164 | #### Guide to placing sounds 165 | 166 | By adding **"?dev=true"** to the end of your URL, you will be able to see the markers which are normally invisible in your application. To start, once you have a street view location, give a sound the same **lat** and **long** values as this location, then find it by walking away from your start position. 167 | 168 | You can then drag any marker around and place it where the sound is being emitted from. The JSON code to insert for this sound will update in the panel, ready to copy and paste into your file. It's important to get positioning accurate so that a sound is heard from the correct source - so walk around the marker, as near as possible and keep dragging and dropping till it seems as close as possible. In the best scenario, walk 'on' the sound source and drag the marker to your feet. 169 | 170 | ![ScreenShot](https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/master/src/img/git-img-5.gif) 171 | 172 | #### Debugging your JSON file 173 | 174 | If you are unfamiliar with writing JSON then it can be frustrating as a simple comma in the wrong 175 | place, or a missing quotation mark can break your application (hint shown in image below). You can use an online [JSON linting 176 | tool](http://jsonlint.com/) to find and remove any errors you may have in your data. 177 | 178 | - [JSONLint](http://jsonlint.com/) 179 | 180 | ![ScreenShot](https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/master/src/img/git-img-2.jpg) 181 | 182 | 183 | ### Creating your sounds 184 | 185 | Creating your sounds can be done in a number of ways. You can either create them yourself, or purchase them from audio sample websites (such as http://audiojungle.net). For a free and easy program to create from scratch or alter sounds, download Audacity at http://audacity.sourceforge.net/download/. 186 | 187 | When exporting your MP3s from Audacity (or other software of your choice), export at as low quality as possible so that your application loads as quickly as possible. Because you will create a range of different sounds, small quality intricacies aren't as noticeable as normal. We recommend 64kbps as shown below. 188 | 189 | ![ScreenShot](https://raw.githubusercontent.com/Amplifon/Sounds-of-Street-View-Framework/master/src/img/git-img-4.jpg) 190 | 191 | 192 | ## Run your application 193 | 194 | In order to get your application to run the last thing you need to do is to create a new 195 | SOSV object in your custom JavaScript. As the project is using jQuery as a dependency the easiest way 196 | is to do it on the jQuery DOM ready. Make sure that the path to your JSON file is correct! 197 | 198 | ```javascript 199 | 204 | ``` 205 | 206 | ## License 207 | 208 | The MIT License 209 | 210 | Copyright (c) 2014 Amplifon 211 | 212 | Permission is hereby granted, free of charge, to any person obtaining a copy 213 | of this software and associated documentation files (the "Software"), to deal 214 | in the Software without restriction, including without limitation the rights 215 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 216 | copies of the Software, and to permit persons to whom the Software is 217 | furnished to do so, subject to the following conditions: 218 | 219 | The above copyright notice and this permission notice shall be included in 220 | all copies or substantial portions of the Software. 221 | 222 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 223 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 224 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 225 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 226 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 227 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 228 | THE SOFTWARE. 229 | -------------------------------------------------------------------------------- /src/js/sosv.js: -------------------------------------------------------------------------------- 1 | 2 | var getUrlVars = function() { 3 | var vars = {}; 4 | var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) { 5 | vars[key] = value; 6 | }); 7 | return vars; 8 | }; 9 | 10 | var devMode = getUrlVars().dev; 11 | 12 | var rad = function(x) { 13 | return x * Math.PI / 180; 14 | }; 15 | 16 | var Distance = function(p1, p2, metric){ 17 | var R = 6378137, // Earth’s mean radius in meter 18 | dLat = rad(p2.lat() - p1.lat()), 19 | dLong = rad(p2.lng() - p1.lng()), 20 | a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(rad(p1.lat())) * Math.cos(rad(p2.lat())) * Math.sin(dLong / 2) * Math.sin(dLong / 2), 21 | c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)), 22 | d = R * c; // d = the distance in meter 23 | 24 | if (metric == 'miles') { // convert to miles? 25 | d = d * 0.000621371192; 26 | } 27 | 28 | return d; 29 | } 30 | 31 | var Sound = function(json, panorama, _sosv){ 32 | 33 | var obj = this; 34 | this.sosv = _sosv; 35 | this.data = json; // JSON data associated with this sound 36 | this.map = panorama; // Street view pano we are working with 37 | 38 | this.sound = null; // Howler object 39 | this.vol = 0; // Volume of sound 40 | 41 | this.position = new google.maps.LatLng(this.data.lat, this.data.lng); 42 | this.prevUserPosition = { lat: null, lng: null }; 43 | this.prevVolume = 0; 44 | 45 | this.init = function(){ 46 | 47 | // Listen for user position change events 48 | $(document.body) 49 | .on('panoChanged', this.onUserMovement) 50 | .on('positionChanged', this.onUserMovement) 51 | .on('povChanged', this.onUserMovement); 52 | 53 | this.createSound(); 54 | this.addSoundToMap(); 55 | }; 56 | 57 | this.createSound = function(){ 58 | 59 | // Only use loop if pause = 0 60 | var loop = (!parseFloat(obj.data.pause)) ? true : false; 61 | 62 | obj.sound = new Howl({ 63 | urls: obj.data.src, 64 | loop: loop, 65 | onload: obj.onSoundLoaded, 66 | onloaderror: obj.onSoundLoadError 67 | }); 68 | }; 69 | 70 | this.onSoundLoaded = function(e){ 71 | 72 | $(document.body).trigger('soundLoaded', obj.data); 73 | }; 74 | 75 | this.onSoundLoadError = function(e){ 76 | 77 | $(document.body).trigger('soundLoadError', obj.data); 78 | }; 79 | 80 | this.addSoundToMap = function(){ 81 | 82 | obj.data.icon = (devMode) ? 'data:image/gif;base64,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' : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; 83 | obj.data.draggable = (devMode) ? true : false; 84 | obj.sosv.addMarker(obj.data); 85 | 86 | this.updatePan(); 87 | }; 88 | 89 | this.playSound = function(){ 90 | 91 | obj.sound.play(); 92 | 93 | // Manually loop the sound, interspesed with pauses, if we have a pause value for this object 94 | if (parseFloat(obj.data.pause)) { 95 | 96 | obj.sound.on('end', function(){ 97 | 98 | obj.sound.pause(); 99 | 100 | setTimeout(function(){ 101 | obj.sound.play(); 102 | }, parseInt(obj.data.pause)); 103 | 104 | }); 105 | } 106 | }; 107 | 108 | this.stopSound = function(){ 109 | obj.sound.stop(); 110 | }; 111 | 112 | this.unloadSound = function(fadeSpeed){ 113 | 114 | obj.sound.fade(obj.vol, 0, fadeSpeed, function(){ 115 | obj.sound.unload(); 116 | }); 117 | 118 | // Sometimes the fade callback does not fire, so manually unload sound after fade length as failsafe 119 | setTimeout(function(){ 120 | if (obj.sound) { 121 | obj.sound.unload(); 122 | } 123 | }, (fadeSpeed+50)); 124 | }; 125 | 126 | this.onUserMovement = function(e, pano){ 127 | 128 | // Get current position data for the user 129 | var lat = pano.getPosition().lat(), 130 | lng = pano.getPosition().lng(), 131 | heading = pano.getPov().heading; 132 | 133 | obj.updatePan(lat, lng, heading); 134 | obj.updateVolume(lat, lng, pano); 135 | }; 136 | 137 | this.updatePan = function(lat, lng, heading){ 138 | 139 | var xDiff = obj.data.lat - lat, 140 | yDiff = obj.data.lng - lng, 141 | angle = Math.atan2(yDiff, xDiff) * (180/Math.PI); 142 | 143 | // Add POV heading offset 144 | angle -= heading; 145 | 146 | // Convert angle to range between -180 and +180 147 | if (angle < -180) angle += 360; 148 | else if (angle > 180) angle -= 360; 149 | 150 | // Calculate panPosition, as a range between -1 and +1 151 | var panPosition = (angle/90); 152 | if (Math.abs(panPosition) > 1) { 153 | var x = Math.abs(panPosition) - 1; 154 | panPosition = (panPosition > 0) ? 1 - x : -1 + x; 155 | } 156 | 157 | // Set the new pan poition 158 | obj.sound.pos3d(panPosition, 1, 1); 159 | 160 | // Apply lowpass filter *if* the sound is behind us (11,000hz = filter fully open) 161 | var freq = 11000; 162 | if (Math.abs(angle) > 90) { 163 | // User's back is to the sound - progressively apply filter 164 | freq -= (Math.abs(angle) - 90) * 55; 165 | } 166 | obj.sound.filter(freq); 167 | }; 168 | 169 | this.updateVolume = function(lat, lng, pano){ 170 | 171 | if (lat !== obj.prevUserPosition.lat || lng !== obj.prevUserPosition.lng) { 172 | 173 | // Calculate distance between user and sound 174 | var distance = Distance(obj.position, pano.getPosition()); 175 | 176 | // Calculate new volume based on distance 177 | obj.vol = obj.calculateVolume(distance); 178 | 179 | // Set new volume 180 | obj.sound.fade(obj.prevVolume, obj.vol, 500); 181 | 182 | // Cache the new volume / position for checking next time 183 | obj.prevVolume = obj.vol; 184 | obj.prevUserPosition.lat = lat; 185 | obj.prevUserPosition.lng = lng; 186 | } 187 | }; 188 | 189 | this.calculateVolume = function(distance){ 190 | // Calculate volume by using Inverse Square Law 191 | obj.vol = 1 / (distance * distance); 192 | // Multiply distance volume by amplitude of sound (apply ceiling max of 1) 193 | obj.vol = Math.min((obj.vol * obj.data.db), 1); 194 | return obj.vol; 195 | }; 196 | 197 | this.init(); 198 | } 199 | 200 | var SOSV = function(jsonPath){ 201 | 202 | var self = this, 203 | el, 204 | panorama, 205 | markers = [], 206 | arrSounds = [], 207 | soundCount = 0; 208 | 209 | this.init = function(){ 210 | 211 | // Test for presence of Web Audio API 212 | if (!this.webApiTest) { 213 | alert('Your browser does not support the Web Audio API!'); 214 | return; 215 | } 216 | 217 | $(document.body) 218 | .on('soundLoaded', this.onSoundLoaded) 219 | .on('soundLoadError', this.onSoundLoaded) 220 | .on('changeLocation', this.onChangeLocation) 221 | .on('panoChanged', this.showUserData) 222 | .on('povChanged', this.showUserData) 223 | .on('positionChanged', this.showUserData) 224 | .on('markerClicked', this.showMarkerData) 225 | .on('markerDragEnd', this.showMarkerData); 226 | 227 | if (devMode) { 228 | this.addDevModeMarkup(); 229 | } 230 | 231 | // Load JSON data 232 | $.getJSON(jsonPath, this.onJsonLoaded); 233 | }; 234 | 235 | this.webApiTest = function(){ 236 | var waAPI; 237 | if (typeof AudioContext !== "undefined") { 238 | waAPI = new AudioContext(); 239 | } else if (typeof webkitAudioContext !== "undefined") { 240 | waAPI = new webkitAudioContext(); 241 | } 242 | return (waAPI) ? true : false; 243 | }; 244 | 245 | this.onJsonLoaded = function(data){ 246 | 247 | soundCount = data.sounds.length; 248 | 249 | self.createStreetView(data); 250 | self.loadSounds(data); 251 | 252 | // Manually trigger onSoundLoaded if there are no sounds in the json data 253 | if (!soundCount) { 254 | self.onSoundLoaded(null); 255 | } 256 | }; 257 | 258 | this.createStreetView = function(data){ 259 | 260 | el = $('#'+data.id); 261 | panorama = new google.maps.StreetViewPanorama(document.getElementById(data.id), { 262 | 263 | position : new google.maps.LatLng(data.lat, data.lng), 264 | pov: { 265 | heading : Number(data.heading), 266 | pitch : Number(data.pitch) 267 | } 268 | }); 269 | // add listeners 270 | google.maps.event.addListener(panorama, 'pano_changed', this.onPanoChanged); 271 | google.maps.event.addListener(panorama, 'position_changed', this.onPositionChanged); 272 | google.maps.event.addListener(panorama, 'pov_changed', this.onPovChanged); 273 | }; 274 | 275 | this.addMarker = function(data){ 276 | 277 | var marker = new google.maps.Marker({ 278 | map : panorama, 279 | title : data.name, 280 | position : new google.maps.LatLng(data.lat, data.lng), 281 | draggable : data.draggable, 282 | icon : data.icon 283 | }); 284 | markers.push(marker); 285 | 286 | google.maps.event.addListener(marker, 'click', function(e) { 287 | $(document.body).trigger('markerClicked', [e, marker, data]); 288 | }); 289 | 290 | google.maps.event.addListener(marker, "dragend", function(e) { 291 | $(document.body).trigger('markerDragEnd', [e, marker, data]); 292 | }); 293 | }; 294 | 295 | this.onPanoChanged = function(e){ 296 | el.trigger('panoChanged', panorama); 297 | }; 298 | 299 | this.onPositionChanged = function(e){ 300 | el.trigger('positionChanged', panorama); 301 | }; 302 | 303 | this.onPovChanged = function(e){ 304 | el.trigger('povChanged', panorama); 305 | }; 306 | 307 | this.loadSounds = function(data){ 308 | 309 | // Create all the sounds objects, and store in array 310 | for (var i=0; i < data.sounds.length; i++) { 311 | var sound = new Sound(data.sounds[i], panorama, self); 312 | arrSounds.push(sound); 313 | } 314 | }; 315 | 316 | this.onSoundLoaded = function(e){ 317 | 318 | soundCount--; 319 | 320 | // All sounds loaded? 321 | if (soundCount <= 0) { 322 | self.playSounds(); 323 | } 324 | }; 325 | 326 | this.playSounds = function(){ 327 | 328 | // Start all sounds and trigger onUserMovement to set filters/pans etc 329 | for (var i=0; i < arrSounds.length; i++) { 330 | arrSounds[i].playSound(); 331 | arrSounds[i].onUserMovement(null, panorama); 332 | } 333 | }; 334 | 335 | this.showUserData = function(e, pano){ 336 | 337 | $('#user-pos') 338 | .find('.user-lat').text(pano.getPosition().lat()).end() 339 | .find('.user-lng').text(pano.getPosition().lng()).end() 340 | .find('.user-heading').text(pano.getPov().heading).end() 341 | .find('.user-pitch').text(pano.getPov().pitch); 342 | }; 343 | 344 | this.showMarkerData = function(e, gEvent, marker, data){ 345 | 346 | var json = data; 347 | delete json["icon"]; 348 | delete json["draggable"]; 349 | json["lat"] = ''+gEvent.latLng.lat(); 350 | json["lng"] = ''+gEvent.latLng.lng(); 351 | 352 | var jsonStr = JSON.stringify(json, null, ' '); 353 | $('#marker-pos').find('.json-pre').html(jsonStr); 354 | }; 355 | 356 | this.addDevModeMarkup = function(){ 357 | 358 | var userPos = $('
'); 359 | userPos.append('

User (you)

').css({'margin':'1em 0 0.3em 0','border-bottom':'1px solid #181818','padding-bottom':'1em'}); 360 | userPos.append('
Lat
Lng
Heading
Pitch
'); 361 | 362 | var markerPos = $('
'); 363 | markerPos.append('

Marker

Drag a marker, then copy and paste the code below to your JSON data file

').css({'margin':'1.7em 0 0.5em 0','border-bottom':'1px solid #181818','padding-bottom':'.4em'}); 364 | markerPos.append('
').css({'line-height':'1.3em'});
365 | 
366 | 		var debugWrap = $('
').append(userPos).append(markerPos).css({'position':'absolute','min-width':'350px','font-family':'sans-serif','font-size':'1em','top':'10px','right':'10px','padding':'1.4em 2em','background':'#fff'}); 367 | $('body').append(debugWrap); 368 | }; 369 | 370 | this.init(); 371 | } -------------------------------------------------------------------------------- /src/js/sosv.min.js: -------------------------------------------------------------------------------- 1 | !function(){var a={},b=null,c=!0,d=!1;try{"undefined"!=typeof AudioContext?b=new AudioContext:"undefined"!=typeof webkitAudioContext?b=new webkitAudioContext:c=!1}catch(e){c=!1}if(!c)if("undefined"!=typeof Audio)try{new Audio}catch(e){d=!0}else d=!0;if(c){var f="undefined"==typeof b.createGain?b.createGainNode():b.createGain();f.gain.value=1,f.connect(b.destination)}var g=function(){this._volume=1,this._muted=!1,this.usingWebAudio=c,this.noAudio=d,this._howls=[]};g.prototype={volume:function(a){var b=this;if(a=parseFloat(a),a>=0&&1>=a){b._volume=a,c&&(f.gain.value=a);for(var d in b._howls)if(b._howls.hasOwnProperty(d)&&b._howls[d]._webAudio===!1)for(var e=0;e=2?f:i.match(/data\:audio\/([^?]+);/),!f)return void b.on("loaderror");f=f[1]}if(j[f]){c=b._urls[e];break}}if(!c)return void b.on("loaderror");if(b._src=c,b._webAudio)l(b,c);else{var k=new Audio;k.addEventListener("error",function(){k.error&&4===k.error.code&&(g.noAudio=!0),b.on("loaderror",{type:k.error?k.error.code:0})},!1),b._audioNode.push(k),k.src=c,k._pos=0,k.preload="auto",k.volume=h._muted?0:b._volume*h.volume(),a[c]=b;var m=function(){b._duration=Math.ceil(10*k.duration)/10,0===Object.getOwnPropertyNames(b._sprite).length&&(b._sprite={_default:[0,1e3*b._duration]}),b._loaded||(b._loaded=!0,b.on("load")),b._autoplay&&b.play(),k.removeEventListener("canplaythrough",m,!1)};k.addEventListener("canplaythrough",m,!1),k.load()}return b},urls:function(a){var b=this;return a?(b.stop(),b._urls="string"==typeof a?[a]:a,b._loaded=!1,b.load(),b):b._urls},play:function(a,c){var d=this;return"function"==typeof a&&(c=a),a&&"function"!=typeof a||(a="_default"),d._loaded?d._sprite[a]?(d._inactiveNode(function(e){e._sprite=a;var f,g=e._pos>0?e._pos:d._sprite[a][0]/1e3,i=d._sprite[a][1]/1e3-e._pos,j=!(!d._loop&&!d._sprite[a][2]),k="string"==typeof c?c:Math.round(Date.now()*Math.random())+"";if(function(){var b={id:k,sprite:a,loop:j};f=setTimeout(function(){!d._webAudio&&j&&d.stop(b.id).play(a,b.id),d._webAudio&&!j&&(d._nodeById(b.id).paused=!0,d._nodeById(b.id)._pos=0),d._webAudio||j||d.stop(b.id),d.on("end",k)},1e3*i),d._onendTimer.push({timer:f,id:b.id})}(),d._webAudio){var l=d._sprite[a][0]/1e3,m=d._sprite[a][1]/1e3;if(e.id=k,e.paused=!1,n(d,[j,l,m],k),d._playStart=b.currentTime,e.gain.value=d._volume,"undefined"==typeof e.bufferSource)return;"undefined"==typeof e.bufferSource.start?e.bufferSource.noteGrainOn(0,g,i):e.bufferSource.start(0,g,i)}else{if(4!==e.readyState&&(e.readyState||!navigator.isCocoonJS))return d._clearEndTimer(k),function(){var b=d,f=a,g=c,h=e,i=function(){b.play(f,g),h.removeEventListener("canplaythrough",i,!1)};h.addEventListener("canplaythrough",i,!1)}(),d;e.readyState=4,e.id=k,e.currentTime=g,e.muted=h._muted||e.muted,e.volume=d._volume*h.volume(),setTimeout(function(){e.play()},0)}return d.on("play"),"function"==typeof c&&c(k),d}),d):("function"==typeof c&&c(),d):(d.on("load",function(){d.play(a,c)}),d)},pause:function(a){var b=this;if(!b._loaded)return b.on("play",function(){b.pause(a)}),b;b._clearEndTimer(a);var c=a?b._nodeById(a):b._activeNode();if(c)if(c._pos=b.pos(null,a),b._webAudio){if(!c.bufferSource||c.paused)return b;c.paused=!0,"undefined"==typeof c.bufferSource.stop?c.bufferSource.noteOff(0):c.bufferSource.stop(0)}else c.pause();return b.on("pause"),b},stop:function(a){var b=this;if(!b._loaded)return b.on("play",function(){b.stop(a)}),b;b._clearEndTimer(a);var c=a?b._nodeById(a):b._activeNode();if(c)if(c._pos=0,b._webAudio){if(!c.bufferSource||c.paused)return b;c.paused=!0,"undefined"==typeof c.bufferSource.stop?c.bufferSource.noteOff(0):c.bufferSource.stop(0)}else isNaN(c.duration)||(c.pause(),c.currentTime=0);return b},mute:function(a){var b=this;if(!b._loaded)return b.on("play",function(){b.mute(a)}),b;var c=a?b._nodeById(a):b._activeNode();return c&&(b._webAudio?c.gain.value=0:c.muted=!0),b},unmute:function(a){var b=this;if(!b._loaded)return b.on("play",function(){b.unmute(a)}),b;var c=a?b._nodeById(a):b._activeNode();return c&&(b._webAudio?c.gain.value=b._volume:c.muted=!1),b},volume:function(a,b){var c=this;if(a=parseFloat(a),a>=0&&1>=a){if(c._volume=a,!c._loaded)return c.on("play",function(){c.volume(a,b)}),c;var d=b?c._nodeById(b):c._activeNode();return d&&(c._webAudio?d.gain.value=a:d.volume=a*h.volume()),c}return c._volume},filter:function(a,b){var c=this;a=parseFloat(a),c._freq=a;var d=b?c._nodeById(b):c._activeNode();return d&&c._webAudio&&(d.filter.frequency.value=a),c},loop:function(a){var b=this;return"boolean"==typeof a?(b._loop=a,b):b._loop},sprite:function(a){var b=this;return"object"==typeof a?(b._sprite=a,b):b._sprite},pos:function(a,c){var d=this;if(!d._loaded)return d.on("load",function(){d.pos(a)}),"number"==typeof a?d:d._pos||0;a=parseFloat(a);var e=c?d._nodeById(c):d._activeNode();if(e)return a>=0?(d.pause(c),e._pos=a,d.play(e._sprite,c),d):d._webAudio?e._pos+(b.currentTime-d._playStart):e.currentTime;if(a>=0)return d;for(var f=0;f=0||0>a))return e._pos3d;if(e._webAudio){var f=d?e._nodeById(d):e._activeNode();f&&(e._pos3d=[a,b,c],f.panner.setPosition(a,b,c),f.panner.panningModel=e._model||"HRTF")}return e},fade:function(a,b,c,d,e){var f=this,g=Math.abs(a-b),h=a>b?"down":"up",i=g/.01,j=c/i;if(!f._loaded)return f.on("load",function(){f.fade(a,b,c,d,e)}),f;f.volume(a,e);for(var k=1;i>=k;k++)!function(){var a=f._volume+("up"===h?.01:-.01)*k,c=Math.round(1e3*a)/1e3,g=b;setTimeout(function(){f.volume(c,e),c===g&&d&&d()},j*k)}()},fadeIn:function(a,b,c){return this.volume(0).play().fade(0,a,b,c)},fadeOut:function(a,b,c,d){var e=this;return e.fade(e._volume,a,b,function(){c&&c(),e.pause(d),e.on("end")},d)},_nodeById:function(a){for(var b=this,c=b._audioNode[0],d=0;d=0&&!(5>=c);a--)b._audioNode[a].paused&&(b._webAudio&&b._audioNode[a].disconnect(0),c--,b._audioNode.splice(a,1))},_clearEndTimer:function(a){for(var b=this,c=0,d=0;d=0&&h._howls.splice(e,1),delete a[b._src],b=null}},c)var l=function(c,d){if(d in a)c._duration=a[d].duration,m(c);else{var e=new XMLHttpRequest;e.open("GET",d,!0),e.responseType="arraybuffer",e.onload=function(){b.decodeAudioData(e.response,function(b){b&&(a[d]=b,m(c,b))},function(){c.on("loaderror")})},e.onerror=function(){c._webAudio&&(c._buffer=!0,c._webAudio=!1,c._audioNode=[],delete c._gainNode,c.load())};try{e.send()}catch(f){e.onerror()}}},m=function(a,b){a._duration=b?b.duration:a._duration,0===Object.getOwnPropertyNames(a._sprite).length&&(a._sprite={_default:[0,1e3*a._duration]}),a._loaded||(a._loaded=!0,a.on("load")),a._autoplay&&a.play()},n=function(c,d,e){var f=c._nodeById(e);"undefined"!=typeof a[c._src]&&(f.bufferSource=b.createBufferSource(),f.bufferSource.buffer=a[c._src],f.bufferSource.connect(f.panner),f.bufferSource.loop=d[0],d[0]&&(f.bufferSource.loopStart=d[1],f.bufferSource.loopEnd=d[1]+d[2]),f.bufferSource.playbackRate.value=c._rate)};"function"==typeof define&&define.amd&&define(function(){return{Howler:h,Howl:k}}),"undefined"!=typeof exports&&(exports.Howler=h,exports.Howl=k),"undefined"!=typeof window&&(window.Howler=h,window.Howl=k)}();var getUrlVars=function(){{var a={};window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(b,c,d){a[c]=d})}return a},devMode=getUrlVars().dev,rad=function(a){return a*Math.PI/180},Distance=function(a,b,c){var d=6378137,e=rad(b.lat()-a.lat()),f=rad(b.lng()-a.lng()),g=Math.sin(e/2)*Math.sin(e/2)+Math.cos(rad(a.lat()))*Math.cos(rad(b.lat()))*Math.sin(f/2)*Math.sin(f/2),h=2*Math.atan2(Math.sqrt(g),Math.sqrt(1-g)),i=d*h;return"miles"==c&&(i=.000621371192*i),i},Sound=function(a,b,c){var d=this;this.sosv=c,this.data=a,this.map=b,this.sound=null,this.vol=0,this.position=new google.maps.LatLng(this.data.lat,this.data.lng),this.prevUserPosition={lat:null,lng:null},this.prevVolume=0,this.init=function(){$(document.body).on("panoChanged",this.onUserMovement).on("positionChanged",this.onUserMovement).on("povChanged",this.onUserMovement),this.createSound(),this.addSoundToMap()},this.createSound=function(){var a=parseFloat(d.data.pause)?!1:!0;d.sound=new Howl({urls:d.data.src,loop:a,onload:d.onSoundLoaded,onloaderror:d.onSoundLoadError})},this.onSoundLoaded=function(){$(document.body).trigger("soundLoaded",d.data)},this.onSoundLoadError=function(){$(document.body).trigger("soundLoadError",d.data)},this.addSoundToMap=function(){d.data.icon=devMode?"data:image/gif;base64,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":"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",d.data.draggable=devMode?!0:!1,d.sosv.addMarker(d.data),this.updatePan()},this.playSound=function(){d.sound.play(),parseFloat(d.data.pause)&&d.sound.on("end",function(){d.sound.pause(),setTimeout(function(){d.sound.play()},parseInt(d.data.pause))})},this.stopSound=function(){d.sound.stop()},this.unloadSound=function(a){d.sound.fade(d.vol,0,a,function(){d.sound.unload()}),setTimeout(function(){d.sound&&d.sound.unload()},a+50)},this.onUserMovement=function(a,b){var c=b.getPosition().lat(),e=b.getPosition().lng(),f=b.getPov().heading;d.updatePan(c,e,f),d.updateVolume(c,e,b)},this.updatePan=function(a,b,c){var e=d.data.lat-a,f=d.data.lng-b,g=Math.atan2(f,e)*(180/Math.PI);g-=c,-180>g?g+=360:g>180&&(g-=360);var h=g/90;if(Math.abs(h)>1){var i=Math.abs(h)-1;h=h>0?1-i:-1+i}d.sound.pos3d(h,1,1);var j=11e3;Math.abs(g)>90&&(j-=55*(Math.abs(g)-90)),d.sound.filter(j)},this.updateVolume=function(a,b,c){if(a!==d.prevUserPosition.lat||b!==d.prevUserPosition.lng){var e=Distance(d.position,c.getPosition());d.vol=d.calculateVolume(e),d.sound.fade(d.prevVolume,d.vol,500),d.prevVolume=d.vol,d.prevUserPosition.lat=a,d.prevUserPosition.lng=b}},this.calculateVolume=function(a){return d.vol=1/(a*a),d.vol=Math.min(d.vol*d.data.db,1),d.vol},this.init()},SOSV=function(a){var b,c,d=this,e=[],f=[],g=0;this.init=function(){return this.webApiTest?($(document.body).on("soundLoaded",this.onSoundLoaded).on("soundLoadError",this.onSoundLoaded).on("changeLocation",this.onChangeLocation).on("panoChanged",this.showUserData).on("povChanged",this.showUserData).on("positionChanged",this.showUserData).on("markerClicked",this.showMarkerData).on("markerDragEnd",this.showMarkerData),devMode&&this.addDevModeMarkup(),void $.getJSON(a,this.onJsonLoaded)):void alert("Your browser does not support the Web Audio API!")},this.webApiTest=function(){var a;return"undefined"!=typeof AudioContext?a=new AudioContext:"undefined"!=typeof webkitAudioContext&&(a=new webkitAudioContext),a?!0:!1},this.onJsonLoaded=function(a){g=a.sounds.length,d.createStreetView(a),d.loadSounds(a),g||d.onSoundLoaded(null)},this.createStreetView=function(a){b=$("#"+a.id),c=new google.maps.StreetViewPanorama(document.getElementById(a.id),{position:new google.maps.LatLng(a.lat,a.lng),pov:{heading:Number(a.heading),pitch:Number(a.pitch)}}),google.maps.event.addListener(c,"pano_changed",this.onPanoChanged),google.maps.event.addListener(c,"position_changed",this.onPositionChanged),google.maps.event.addListener(c,"pov_changed",this.onPovChanged)},this.addMarker=function(a){var b=new google.maps.Marker({map:c,title:a.name,position:new google.maps.LatLng(a.lat,a.lng),draggable:a.draggable,icon:a.icon});e.push(b),google.maps.event.addListener(b,"click",function(c){$(document.body).trigger("markerClicked",[c,b,a])}),google.maps.event.addListener(b,"dragend",function(c){$(document.body).trigger("markerDragEnd",[c,b,a])})},this.onPanoChanged=function(){b.trigger("panoChanged",c)},this.onPositionChanged=function(){b.trigger("positionChanged",c)},this.onPovChanged=function(){b.trigger("povChanged",c)},this.loadSounds=function(a){for(var b=0;b=g&&d.playSounds()},this.playSounds=function(){for(var a=0;a');a.append("

User (you)

").css({margin:"1em 0 0.3em 0","border-bottom":"1px solid #181818","padding-bottom":"1em"}),a.append('
Lat
Lng
Heading
Pitch
');var b=$('
');b.append('

Marker

Drag a marker, then copy and paste the code below to your JSON data file

').css({margin:"1.7em 0 0.5em 0","border-bottom":"1px solid #181818","padding-bottom":".4em"}),b.append('
').css({"line-height":"1.3em"});var c=$('
').append(a).append(b).css({position:"absolute","min-width":"350px","font-family":"sans-serif","font-size":"1em",top:"10px",right:"10px",padding:"1.4em 2em",background:"#fff"});$("body").append(c)},this.init()}; -------------------------------------------------------------------------------- /src/js/howler.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * howler.js v1.1.20 3 | * howlerjs.com 4 | * 5 | * (c) 2013-2014, James Simpson of GoldFire Studios 6 | * goldfirestudios.com 7 | * 8 | * MIT License 9 | */ 10 | 11 | (function() { 12 | // setup 13 | var cache = {}; 14 | 15 | // setup the audio context 16 | var ctx = null, 17 | usingWebAudio = true, 18 | noAudio = false; 19 | try { 20 | if (typeof AudioContext !== 'undefined') { 21 | ctx = new AudioContext(); 22 | } else if (typeof webkitAudioContext !== 'undefined') { 23 | ctx = new webkitAudioContext(); 24 | } else { 25 | usingWebAudio = false; 26 | } 27 | } catch(e) { 28 | usingWebAudio = false; 29 | } 30 | 31 | if (!usingWebAudio) { 32 | if (typeof Audio !== 'undefined') { 33 | try { 34 | new Audio(); 35 | } catch(e) { 36 | noAudio = true; 37 | } 38 | } else { 39 | noAudio = true; 40 | } 41 | } 42 | 43 | // create a master gain node 44 | if (usingWebAudio) { 45 | var masterGain = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain(); 46 | masterGain.gain.value = 1; 47 | masterGain.connect(ctx.destination); 48 | } 49 | 50 | // create global controller 51 | var HowlerGlobal = function() { 52 | this._volume = 1; 53 | this._muted = false; 54 | this.usingWebAudio = usingWebAudio; 55 | this.noAudio = noAudio; 56 | this._howls = []; 57 | }; 58 | HowlerGlobal.prototype = { 59 | /** 60 | * Get/set the global volume for all sounds. 61 | * @param {Float} vol Volume from 0.0 to 1.0. 62 | * @return {Howler/Float} Returns self or current volume. 63 | */ 64 | volume: function(vol) { 65 | var self = this; 66 | 67 | // make sure volume is a number 68 | vol = parseFloat(vol); 69 | 70 | if (vol >= 0 && vol <= 1) { 71 | self._volume = vol; 72 | 73 | if (usingWebAudio) { 74 | masterGain.gain.value = vol; 75 | } 76 | 77 | // loop through cache and change volume of all nodes that are using HTML5 Audio 78 | for (var key in self._howls) { 79 | if (self._howls.hasOwnProperty(key) && self._howls[key]._webAudio === false) { 80 | // loop through the audio nodes 81 | for (var i=0; i= 2) ? ext : urlItem.match(/data\:audio\/([^?]+);/); 232 | 233 | if (ext) { 234 | ext = ext[1]; 235 | } else { 236 | self.on('loaderror'); 237 | return; 238 | } 239 | } 240 | 241 | if (codecs[ext]) { 242 | url = self._urls[i]; 243 | break; 244 | } 245 | } 246 | 247 | if (!url) { 248 | self.on('loaderror'); 249 | return; 250 | } 251 | 252 | self._src = url; 253 | 254 | if (self._webAudio) { 255 | loadBuffer(self, url); 256 | } else { 257 | var newNode = new Audio(); 258 | 259 | // listen for errors with HTML5 audio (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror) 260 | newNode.addEventListener('error', function () { 261 | if (newNode.error && newNode.error.code === 4) { 262 | HowlerGlobal.noAudio = true; 263 | } 264 | 265 | self.on('loaderror', {type: newNode.error ? newNode.error.code : 0}); 266 | }, false); 267 | 268 | self._audioNode.push(newNode); 269 | 270 | // setup the new audio node 271 | newNode.src = url; 272 | newNode._pos = 0; 273 | newNode.preload = 'auto'; 274 | newNode.volume = (Howler._muted) ? 0 : self._volume * Howler.volume(); 275 | 276 | // add this sound to the cache 277 | cache[url] = self; 278 | 279 | // setup the event listener to start playing the sound 280 | // as soon as it has buffered enough 281 | var listener = function() { 282 | // round up the duration when using HTML5 Audio to account for the lower precision 283 | self._duration = Math.ceil(newNode.duration * 10) / 10; 284 | 285 | // setup a sprite if none is defined 286 | if (Object.getOwnPropertyNames(self._sprite).length === 0) { 287 | self._sprite = {_default: [0, self._duration * 1000]}; 288 | } 289 | 290 | if (!self._loaded) { 291 | self._loaded = true; 292 | self.on('load'); 293 | } 294 | 295 | if (self._autoplay) { 296 | self.play(); 297 | } 298 | 299 | // clear the event listener 300 | newNode.removeEventListener('canplaythrough', listener, false); 301 | }; 302 | newNode.addEventListener('canplaythrough', listener, false); 303 | newNode.load(); 304 | } 305 | 306 | return self; 307 | }, 308 | 309 | /** 310 | * Get/set the URLs to be pulled from to play in this source. 311 | * @param {Array} urls Arry of URLs to load from 312 | * @return {Howl} Returns self or the current URLs 313 | */ 314 | urls: function(urls) { 315 | var self = this; 316 | 317 | if (urls) { 318 | self.stop(); 319 | self._urls = (typeof urls === 'string') ? [urls] : urls; 320 | self._loaded = false; 321 | self.load(); 322 | 323 | return self; 324 | } else { 325 | return self._urls; 326 | } 327 | }, 328 | 329 | /** 330 | * Play a sound from the current time (0 by default). 331 | * @param {String} sprite (optional) Plays from the specified position in the sound sprite definition. 332 | * @param {Function} callback (optional) Returns the unique playback id for this sound instance. 333 | * @return {Howl} 334 | */ 335 | play: function(sprite, callback) { 336 | var self = this; 337 | 338 | // if no sprite was passed but a callback was, update the variables 339 | if (typeof sprite === 'function') { 340 | callback = sprite; 341 | } 342 | 343 | // use the default sprite if none is passed 344 | if (!sprite || typeof sprite === 'function') { 345 | sprite = '_default'; 346 | } 347 | 348 | // if the sound hasn't been loaded, add it to the event queue 349 | if (!self._loaded) { 350 | self.on('load', function() { 351 | self.play(sprite, callback); 352 | }); 353 | 354 | return self; 355 | } 356 | 357 | // if the sprite doesn't exist, play nothing 358 | if (!self._sprite[sprite]) { 359 | if (typeof callback === 'function') callback(); 360 | return self; 361 | } 362 | 363 | // get the node to playback 364 | self._inactiveNode(function(node) { 365 | // persist the sprite being played 366 | node._sprite = sprite; 367 | 368 | // determine where to start playing from 369 | var pos = (node._pos > 0) ? node._pos : self._sprite[sprite][0] / 1000, 370 | duration = self._sprite[sprite][1] / 1000 - node._pos; 371 | 372 | // determine if this sound should be looped 373 | var loop = !!(self._loop || self._sprite[sprite][2]); 374 | 375 | // set timer to fire the 'onend' event 376 | var soundId = (typeof callback === 'string') ? callback : Math.round(Date.now() * Math.random()) + '', 377 | timerId; 378 | (function() { 379 | var data = { 380 | id: soundId, 381 | sprite: sprite, 382 | loop: loop 383 | }; 384 | timerId = setTimeout(function() { 385 | // if looping, restart the track 386 | if (!self._webAudio && loop) { 387 | self.stop(data.id).play(sprite, data.id); 388 | } 389 | 390 | // set web audio node to paused at end 391 | if (self._webAudio && !loop) { 392 | self._nodeById(data.id).paused = true; 393 | self._nodeById(data.id)._pos = 0; 394 | } 395 | 396 | // end the track if it is HTML audio and a sprite 397 | if (!self._webAudio && !loop) { 398 | self.stop(data.id); 399 | } 400 | 401 | // fire ended event 402 | self.on('end', soundId); 403 | }, duration * 1000); 404 | 405 | // store the reference to the timer 406 | self._onendTimer.push({timer: timerId, id: data.id}); 407 | })(); 408 | 409 | if (self._webAudio) { 410 | var loopStart = self._sprite[sprite][0] / 1000, 411 | loopEnd = self._sprite[sprite][1] / 1000; 412 | 413 | // set the play id to this node and load into context 414 | node.id = soundId; 415 | node.paused = false; 416 | refreshBuffer(self, [loop, loopStart, loopEnd], soundId); 417 | self._playStart = ctx.currentTime; 418 | node.gain.value = self._volume; 419 | 420 | if (typeof node.bufferSource === 'undefined') { 421 | return; 422 | } 423 | 424 | if (typeof node.bufferSource.start === 'undefined') { 425 | node.bufferSource.noteGrainOn(0, pos, duration); 426 | } else { 427 | node.bufferSource.start(0, pos, duration); 428 | } 429 | } else { 430 | if (node.readyState === 4 || !node.readyState && navigator.isCocoonJS) { 431 | node.readyState = 4; 432 | node.id = soundId; 433 | node.currentTime = pos; 434 | node.muted = Howler._muted || node.muted; 435 | node.volume = self._volume * Howler.volume(); 436 | setTimeout(function() { node.play(); }, 0); 437 | } else { 438 | self._clearEndTimer(soundId); 439 | 440 | (function(){ 441 | var sound = self, 442 | playSprite = sprite, 443 | fn = callback, 444 | newNode = node; 445 | var listener = function() { 446 | sound.play(playSprite, fn); 447 | 448 | // clear the event listener 449 | newNode.removeEventListener('canplaythrough', listener, false); 450 | }; 451 | newNode.addEventListener('canplaythrough', listener, false); 452 | })(); 453 | 454 | return self; 455 | } 456 | } 457 | 458 | // fire the play event and send the soundId back in the callback 459 | self.on('play'); 460 | if (typeof callback === 'function') callback(soundId); 461 | 462 | return self; 463 | }); 464 | 465 | return self; 466 | }, 467 | 468 | /** 469 | * Pause playback and save the current position. 470 | * @param {String} id (optional) The play instance ID. 471 | * @return {Howl} 472 | */ 473 | pause: function(id) { 474 | var self = this; 475 | 476 | // if the sound hasn't been loaded, add it to the event queue 477 | if (!self._loaded) { 478 | self.on('play', function() { 479 | self.pause(id); 480 | }); 481 | 482 | return self; 483 | } 484 | 485 | // clear 'onend' timer 486 | self._clearEndTimer(id); 487 | 488 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 489 | if (activeNode) { 490 | activeNode._pos = self.pos(null, id); 491 | 492 | if (self._webAudio) { 493 | // make sure the sound has been created 494 | if (!activeNode.bufferSource || activeNode.paused) { 495 | return self; 496 | } 497 | 498 | activeNode.paused = true; 499 | if (typeof activeNode.bufferSource.stop === 'undefined') { 500 | activeNode.bufferSource.noteOff(0); 501 | } else { 502 | activeNode.bufferSource.stop(0); 503 | } 504 | } else { 505 | activeNode.pause(); 506 | } 507 | } 508 | 509 | self.on('pause'); 510 | 511 | return self; 512 | }, 513 | 514 | /** 515 | * Stop playback and reset to start. 516 | * @param {String} id (optional) The play instance ID. 517 | * @return {Howl} 518 | */ 519 | stop: function(id) { 520 | var self = this; 521 | 522 | // if the sound hasn't been loaded, add it to the event queue 523 | if (!self._loaded) { 524 | self.on('play', function() { 525 | self.stop(id); 526 | }); 527 | 528 | return self; 529 | } 530 | 531 | // clear 'onend' timer 532 | self._clearEndTimer(id); 533 | 534 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 535 | if (activeNode) { 536 | activeNode._pos = 0; 537 | 538 | if (self._webAudio) { 539 | // make sure the sound has been created 540 | if (!activeNode.bufferSource || activeNode.paused) { 541 | return self; 542 | } 543 | 544 | activeNode.paused = true; 545 | 546 | if (typeof activeNode.bufferSource.stop === 'undefined') { 547 | activeNode.bufferSource.noteOff(0); 548 | } else { 549 | activeNode.bufferSource.stop(0); 550 | } 551 | } else if (!isNaN(activeNode.duration)) { 552 | activeNode.pause(); 553 | activeNode.currentTime = 0; 554 | } 555 | } 556 | 557 | return self; 558 | }, 559 | 560 | /** 561 | * Mute this sound. 562 | * @param {String} id (optional) The play instance ID. 563 | * @return {Howl} 564 | */ 565 | mute: function(id) { 566 | var self = this; 567 | 568 | // if the sound hasn't been loaded, add it to the event queue 569 | if (!self._loaded) { 570 | self.on('play', function() { 571 | self.mute(id); 572 | }); 573 | 574 | return self; 575 | } 576 | 577 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 578 | if (activeNode) { 579 | if (self._webAudio) { 580 | activeNode.gain.value = 0; 581 | } else { 582 | activeNode.muted = true; 583 | } 584 | } 585 | 586 | return self; 587 | }, 588 | 589 | /** 590 | * Unmute this sound. 591 | * @param {String} id (optional) The play instance ID. 592 | * @return {Howl} 593 | */ 594 | unmute: function(id) { 595 | var self = this; 596 | 597 | // if the sound hasn't been loaded, add it to the event queue 598 | if (!self._loaded) { 599 | self.on('play', function() { 600 | self.unmute(id); 601 | }); 602 | 603 | return self; 604 | } 605 | 606 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 607 | if (activeNode) { 608 | if (self._webAudio) { 609 | activeNode.gain.value = self._volume; 610 | } else { 611 | activeNode.muted = false; 612 | } 613 | } 614 | 615 | return self; 616 | }, 617 | 618 | /** 619 | * Get/set volume of this sound. 620 | * @param {Float} vol Volume from 0.0 to 1.0. 621 | * @param {String} id (optional) The play instance ID. 622 | * @return {Howl/Float} Returns self or current volume. 623 | */ 624 | volume: function(vol, id) { 625 | var self = this; 626 | 627 | // make sure volume is a number 628 | vol = parseFloat(vol); 629 | 630 | if (vol >= 0 && vol <= 1) { 631 | self._volume = vol; 632 | 633 | // if the sound hasn't been loaded, add it to the event queue 634 | if (!self._loaded) { 635 | self.on('play', function() { 636 | self.volume(vol, id); 637 | }); 638 | 639 | return self; 640 | } 641 | 642 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 643 | if (activeNode) { 644 | if (self._webAudio) { 645 | activeNode.gain.value = vol; 646 | } else { 647 | activeNode.volume = vol * Howler.volume(); 648 | } 649 | } 650 | 651 | return self; 652 | } else { 653 | return self._volume; 654 | } 655 | }, 656 | 657 | filter: function(freq, id){ 658 | var self = this; 659 | 660 | // make sure gain is a number 661 | freq = parseFloat(freq); 662 | 663 | self._freq = freq; 664 | 665 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 666 | if (activeNode) { 667 | if (self._webAudio) { 668 | activeNode.filter.frequency.value = freq; 669 | } 670 | } 671 | return self; 672 | }, 673 | 674 | /** 675 | * Get/set whether to loop the sound. 676 | * @param {Boolean} loop To loop or not to loop, that is the question. 677 | * @return {Howl/Boolean} Returns self or current looping value. 678 | */ 679 | loop: function(loop) { 680 | var self = this; 681 | 682 | if (typeof loop === 'boolean') { 683 | self._loop = loop; 684 | 685 | return self; 686 | } else { 687 | return self._loop; 688 | } 689 | }, 690 | 691 | /** 692 | * Get/set sound sprite definition. 693 | * @param {Object} sprite Example: {spriteName: [offset, duration, loop]} 694 | * @param {Integer} offset Where to begin playback in milliseconds 695 | * @param {Integer} duration How long to play in milliseconds 696 | * @param {Boolean} loop (optional) Set true to loop this sprite 697 | * @return {Howl} Returns current sprite sheet or self. 698 | */ 699 | sprite: function(sprite) { 700 | var self = this; 701 | 702 | if (typeof sprite === 'object') { 703 | self._sprite = sprite; 704 | 705 | return self; 706 | } else { 707 | return self._sprite; 708 | } 709 | }, 710 | 711 | /** 712 | * Get/set the position of playback. 713 | * @param {Float} pos The position to move current playback to. 714 | * @param {String} id (optional) The play instance ID. 715 | * @return {Howl/Float} Returns self or current playback position. 716 | */ 717 | pos: function(pos, id) { 718 | var self = this; 719 | 720 | // if the sound hasn't been loaded, add it to the event queue 721 | if (!self._loaded) { 722 | self.on('load', function() { 723 | self.pos(pos); 724 | }); 725 | 726 | return typeof pos === 'number' ? self : self._pos || 0; 727 | } 728 | 729 | // make sure we are dealing with a number for pos 730 | pos = parseFloat(pos); 731 | 732 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 733 | if (activeNode) { 734 | if (pos >= 0) { 735 | self.pause(id); 736 | activeNode._pos = pos; 737 | self.play(activeNode._sprite, id); 738 | 739 | return self; 740 | } else { 741 | return self._webAudio ? activeNode._pos + (ctx.currentTime - self._playStart) : activeNode.currentTime; 742 | } 743 | } else if (pos >= 0) { 744 | return self; 745 | } else { 746 | // find the first inactive node to return the pos for 747 | for (var i=0; i= 0 || x < 0) { 785 | if (self._webAudio) { 786 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 787 | if (activeNode) { 788 | self._pos3d = [x, y, z]; 789 | activeNode.panner.setPosition(x, y, z); 790 | activeNode.panner.panningModel = self._model || 'HRTF'; 791 | } 792 | } 793 | } else { 794 | return self._pos3d; 795 | } 796 | 797 | return self; 798 | }, 799 | 800 | /** 801 | * Fade a currently playing sound between two volumes. 802 | * @param {Number} from The volume to fade from (0.0 to 1.0). 803 | * @param {Number} to The volume to fade to (0.0 to 1.0). 804 | * @param {Number} len Time in milliseconds to fade. 805 | * @param {Function} callback (optional) Fired when the fade is complete. 806 | * @param {String} id (optional) The play instance ID. 807 | * @return {Howl} 808 | */ 809 | fade: function(from, to, len, callback, id) { 810 | var self = this, 811 | diff = Math.abs(from - to), 812 | dir = from > to ? 'down' : 'up', 813 | steps = diff / 0.01, 814 | stepTime = len / steps; 815 | 816 | // if the sound hasn't been loaded, add it to the event queue 817 | if (!self._loaded) { 818 | self.on('load', function() { 819 | self.fade(from, to, len, callback, id); 820 | }); 821 | 822 | return self; 823 | } 824 | 825 | // set the volume to the start position 826 | self.volume(from, id); 827 | 828 | for (var i=1; i<=steps; i++) { 829 | (function() { 830 | var change = self._volume + (dir === 'up' ? 0.01 : -0.01) * i, 831 | vol = Math.round(1000 * change) / 1000, 832 | toVol = to; 833 | 834 | setTimeout(function() { 835 | self.volume(vol, id); 836 | 837 | if (vol === toVol) { 838 | if (callback) callback(); 839 | } 840 | }, stepTime * i); 841 | })(); 842 | } 843 | }, 844 | 845 | /** 846 | * [DEPRECATED] Fade in the current sound. 847 | * @param {Float} to Volume to fade to (0.0 to 1.0). 848 | * @param {Number} len Time in milliseconds to fade. 849 | * @param {Function} callback 850 | * @return {Howl} 851 | */ 852 | fadeIn: function(to, len, callback) { 853 | return this.volume(0).play().fade(0, to, len, callback); 854 | }, 855 | 856 | /** 857 | * [DEPRECATED] Fade out the current sound and pause when finished. 858 | * @param {Float} to Volume to fade to (0.0 to 1.0). 859 | * @param {Number} len Time in milliseconds to fade. 860 | * @param {Function} callback 861 | * @param {String} id (optional) The play instance ID. 862 | * @return {Howl} 863 | */ 864 | fadeOut: function(to, len, callback, id) { 865 | var self = this; 866 | 867 | return self.fade(self._volume, to, len, function() { 868 | if (callback) callback(); 869 | self.pause(id); 870 | 871 | // fire ended event 872 | self.on('end'); 873 | }, id); 874 | }, 875 | 876 | /** 877 | * Get an audio node by ID. 878 | * @return {Howl} Audio node. 879 | */ 880 | _nodeById: function(id) { 881 | var self = this, 882 | node = self._audioNode[0]; 883 | 884 | // find the node with this ID 885 | for (var i=0; i=0; i--) { 974 | if (inactive <= 5) { 975 | break; 976 | } 977 | 978 | if (self._audioNode[i].paused) { 979 | // disconnect the audio source if using Web Audio 980 | if (self._webAudio) { 981 | self._audioNode[i].disconnect(0); 982 | } 983 | 984 | inactive--; 985 | self._audioNode.splice(i, 1); 986 | } 987 | } 988 | }, 989 | 990 | /** 991 | * Clear 'onend' timeout before it ends. 992 | * @param {String} soundId The play instance ID. 993 | */ 994 | _clearEndTimer: function(soundId) { 995 | var self = this, 996 | index = 0; 997 | 998 | // loop through the timers to find the one associated with this sound 999 | for (var i=0; i= 0) { 1129 | Howler._howls.splice(index, 1); 1130 | } 1131 | 1132 | // delete this sound from the cache 1133 | delete cache[self._src]; 1134 | self = null; 1135 | } 1136 | 1137 | }; 1138 | 1139 | // only define these functions when using WebAudio 1140 | if (usingWebAudio) { 1141 | 1142 | /** 1143 | * Buffer a sound from URL (or from cache) and decode to audio source (Web Audio API). 1144 | * @param {Object} obj The Howl object for the sound to load. 1145 | * @param {String} url The path to the sound file. 1146 | */ 1147 | var loadBuffer = function(obj, url) { 1148 | // check if the buffer has already been cached 1149 | if (url in cache) { 1150 | // set the duration from the cache 1151 | obj._duration = cache[url].duration; 1152 | 1153 | // load the sound into this object 1154 | loadSound(obj); 1155 | } else { 1156 | // load the buffer from the URL 1157 | var xhr = new XMLHttpRequest(); 1158 | xhr.open('GET', url, true); 1159 | xhr.responseType = 'arraybuffer'; 1160 | xhr.onload = function() { 1161 | // decode the buffer into an audio source 1162 | ctx.decodeAudioData( 1163 | xhr.response, 1164 | function(buffer) { 1165 | if (buffer) { 1166 | cache[url] = buffer; 1167 | loadSound(obj, buffer); 1168 | } 1169 | }, 1170 | function(err) { 1171 | obj.on('loaderror'); 1172 | } 1173 | ); 1174 | }; 1175 | xhr.onerror = function() { 1176 | // if there is an error, switch the sound to HTML Audio 1177 | if (obj._webAudio) { 1178 | obj._buffer = true; 1179 | obj._webAudio = false; 1180 | obj._audioNode = []; 1181 | delete obj._gainNode; 1182 | obj.load(); 1183 | } 1184 | }; 1185 | try { 1186 | xhr.send(); 1187 | } catch (e) { 1188 | xhr.onerror(); 1189 | } 1190 | } 1191 | }; 1192 | 1193 | /** 1194 | * Finishes loading the Web Audio API sound and fires the loaded event 1195 | * @param {Object} obj The Howl object for the sound to load. 1196 | * @param {Objecct} buffer The decoded buffer sound source. 1197 | */ 1198 | var loadSound = function(obj, buffer) { 1199 | // set the duration 1200 | obj._duration = (buffer) ? buffer.duration : obj._duration; 1201 | 1202 | // setup a sprite if none is defined 1203 | if (Object.getOwnPropertyNames(obj._sprite).length === 0) { 1204 | obj._sprite = {_default: [0, obj._duration * 1000]}; 1205 | } 1206 | 1207 | // fire the loaded event 1208 | if (!obj._loaded) { 1209 | obj._loaded = true; 1210 | obj.on('load'); 1211 | } 1212 | 1213 | if (obj._autoplay) { 1214 | obj.play(); 1215 | } 1216 | }; 1217 | 1218 | /** 1219 | * Load the sound back into the buffer source. 1220 | * @param {Object} obj The sound to load. 1221 | * @param {Array} loop Loop boolean, pos, and duration. 1222 | * @param {String} id (optional) The play instance ID. 1223 | */ 1224 | var refreshBuffer = function(obj, loop, id) { 1225 | // determine which node to connect to 1226 | var node = obj._nodeById(id); 1227 | 1228 | if (typeof cache[obj._src] === "undefined"){ 1229 | return; 1230 | } 1231 | 1232 | // setup the buffer source for playback 1233 | node.bufferSource = ctx.createBufferSource(); 1234 | node.bufferSource.buffer = cache[obj._src]; 1235 | node.bufferSource.connect(node.panner); 1236 | node.bufferSource.loop = loop[0]; 1237 | if (loop[0]) { 1238 | node.bufferSource.loopStart = loop[1]; 1239 | node.bufferSource.loopEnd = loop[1] + loop[2]; 1240 | } 1241 | node.bufferSource.playbackRate.value = obj._rate; 1242 | }; 1243 | 1244 | } 1245 | 1246 | /** 1247 | * Add support for AMD (Asynchronous Module Definition) libraries such as require.js. 1248 | */ 1249 | if (typeof define === 'function' && define.amd) { 1250 | define(function() { 1251 | return { 1252 | Howler: Howler, 1253 | Howl: Howl 1254 | }; 1255 | }); 1256 | } 1257 | 1258 | /** 1259 | * Add support for CommonJS libraries such as browserify. 1260 | */ 1261 | if (typeof exports !== 'undefined') { 1262 | exports.Howler = Howler; 1263 | exports.Howl = Howl; 1264 | } 1265 | 1266 | // define globally in case AMD is not available or available but not used 1267 | 1268 | if (typeof window !== 'undefined') { 1269 | window.Howler = Howler; 1270 | window.Howl = Howl; 1271 | } 1272 | 1273 | })(); 1274 | --------------------------------------------------------------------------------