├── .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 |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 | [](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 |  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 |  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 | [](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 |  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 |  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 |  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('| Lat | |
| Lng | |
| Heading | |
| Pitch |
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| Lat | |
| Lng | |
| Heading | |
| Pitch |
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