├── .gitignore ├── LICENSE ├── README.md ├── css ├── bootstrap.css ├── bootstrap.min.css ├── images │ ├── head_logo.gif │ ├── irt_48x48.png │ ├── ui-bg_diagonals-thick_18_b81900_40x40.png │ ├── ui-bg_diagonals-thick_20_666666_40x40.png │ ├── ui-bg_flat_10_000000_40x100.png │ ├── ui-bg_glass_100_f6f6f6_1x400.png │ ├── ui-bg_glass_100_fdf5ce_1x400.png │ ├── ui-bg_glass_65_ffffff_1x400.png │ ├── ui-bg_gloss-wave_35_f6a828_500x100.png │ ├── ui-bg_highlight-soft_100_eeeeee_1x100.png │ ├── ui-bg_highlight-soft_75_ffe45c_1x100.png │ ├── ui-icons_222222_256x240.png │ ├── ui-icons_228ef1_256x240.png │ ├── ui-icons_ef8c08_256x240.png │ ├── ui-icons_ffd27a_256x240.png │ └── ui-icons_ffffff_256x240.png ├── jquery-ui.min.css ├── logo-nav.css └── ui.css ├── dist ├── bogJS-latest.js ├── bogJS-latest.js.map ├── bogJS-latest.min.js ├── bogJS-latest.min.js.map ├── bogJS-ui-latest.js ├── bogJS-ui-latest.js.map ├── bogJS-ui-latest.min.js └── bogJS-ui-latest.min.js.map ├── img ├── control.png ├── control_disabled.png ├── headphones.png ├── headphones_disabled.png ├── listener.png ├── ls.png ├── ls_disabled.png ├── pause.png ├── play.png ├── play_disabled.png ├── stereo.png ├── stop.png └── stop_disabled.png ├── index-ui.js ├── index.html ├── index.js ├── jsdoc.conf ├── package-lock.json ├── package.json ├── src ├── channelorder_test.js ├── gain_controller.js ├── html5_player │ └── core.js ├── media_controller.js ├── object.js ├── object_manager.js ├── scene_reader.js └── ui.js └── tools ├── Multichannel-Order_Browsertest.xlsx ├── create_channelOrder_testfiles.sh ├── encodeMultichannel.sh └── zip_demo.sh /.gitignore: -------------------------------------------------------------------------------- 1 | signals 2 | out 3 | scenes/ 4 | !scenes/debo.spatdif 5 | _site 6 | bogJS_demo.zip 7 | bogJS_demo 8 | ~$Multichannel-Order_Browsertest.xlsx 9 | dist/* 10 | !dist/bogJS-latest* 11 | !dist/bogJS-ui-latest* 12 | doc 13 | demos/ 14 | js/ 15 | node_modules 16 | yarn.lock 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Institut für Rundfunktechnik GmbH (IRT) 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bogJS - JS framework for object-based audio rendering in browsers 2 | 3 | With the introduction of [HTML5](https://en.wikipedia.org/wiki/HTML5_Audio) and the 4 | [Web Audio API](https://webaudio.github.io/web-audio-api/), 5 | an important prerequisite was made for native rendering of object-based audio 6 | in modern browsers. 7 | Object-based audio is a revolutionary approach for creating and deploying 8 | interactive, personalised, scalable and immersive content, by representing it as 9 | a set of individual assets together with metadata describing their relationships 10 | and associations. This allows media objects to be assembled in ground-breaking 11 | ways to create new user experiences. 12 | 13 | ## Demo 14 | See bogJS in action: https://lab.irt.de/demos/object-based-audio/RadioDrama/ 15 | 16 | 17 | ## Basic usage and sample scene 18 | ```javascript 19 | 23 | 24 | 25 | 26 | 27 | 28 | 36 | ``` 37 | ##### sceneFile.spatdif 38 | ```spatdif 39 | /spatdif/time 0.0 40 | /spatdif/source/Drums/position 0.01 1.24 0 41 | /spatdif/source/Drums/interpolate 0 42 | /spatdif/source/Drums/active 1 43 | /spatdif/source/Drums/track_mapping http://akamai-progressive.irt.de/demos/bogJS/debo/drums 44 | /spatdif/source/Drums/gain 1 45 | /spatdif/source/Drums/interactive 0 46 | /spatdif/source/Bass/position -0.85 2.55 0 47 | /spatdif/source/Bass/interpolate 0 48 | /spatdif/source/Bass/active 1 49 | /spatdif/source/Bass/track_mapping http://akamai-progressive.irt.de/demos/bogJS/debo/bass 50 | /spatdif/source/Bass/gain 1 51 | /spatdif/source/Bass/interactive 0 52 | /spatdif/source/Vocal/position -1.06 0.52 0 53 | /spatdif/source/Vocal/interpolate 0 54 | /spatdif/source/Vocal/active 1 55 | /spatdif/source/Vocal/track_mapping http://akamai-progressive.irt.de/demos/bogJS/debo/vocal 56 | /spatdif/source/Vocal/gain 1 57 | /spatdif/source/Vocal/interactive 0 58 | /spatdif/source/Accordion/position 1.82 1.47 0 59 | /spatdif/source/Accordion/interpolate 0 60 | /spatdif/source/Accordion/active 1 61 | /spatdif/source/Accordion/track_mapping http://akamai-progressive.irt.de/demos/bogJS/debo/accordion 62 | /spatdif/source/Accordion/gain 1 63 | /spatdif/source/Accordion/interactive 0 64 | /spatdif/source/Guitar/position 1.8 2.23 0 65 | /spatdif/source/Guitar/interpolate 0 66 | /spatdif/source/Guitar/active 1 67 | /spatdif/source/Guitar/track_mapping http://akamai-progressive.irt.de/demos/bogJS/debo/guitar 68 | /spatdif/source/Guitar/gain 1 69 | /spatdif/source/Guitar/interactive 0 70 | /spatdif/source/Trumpet/position -1.92 -0.59 0 71 | /spatdif/source/Trumpet/interpolate 0 72 | /spatdif/source/Trumpet/active 1 73 | /spatdif/source/Trumpet/track_mapping http://akamai-progressive.irt.de/demos/bogJS/debo/trumpet 74 | /spatdif/source/Trumpet/gain 1 75 | /spatdif/source/Trumpet/interactive 0 76 | ``` 77 | 78 | ## Installation 79 | #### Using the latest version 80 | For your convenience, the dist/ folder contains the latest build files of 81 | bogJS. 82 | 83 | #### Building by yourself 84 | You only need [npm](https://www.npmjs.com/) to build this framework. 85 | Once you have npm installed, you must run 86 | 87 | ```shell 88 | npm install 89 | ``` 90 | to install the required dependencies. 91 | 92 | Now, you can build the project with several commands: 93 | 94 | To build and bundle the whole project, run this command in the project folder. 95 | This will create another subfolder "dist/" with the files as follows: 96 | 97 | ```shell 98 | npm run build 99 | 100 | --> dist/bogJS-latest.js 101 | --> dist/bogJS-latest.js.map 102 | --> dist/bogJS-latest.min.js 103 | --> dist/bogJS-latest.min.js.map 104 | --> dist/bogJS-ui-latest.js 105 | --> dist/bogJS-ui-latest.js.map 106 | --> dist/bogJS-ui-latest.min.js 107 | --> dist/bogJS-ui-latest.min.js.map 108 | ``` 109 | 110 | 111 | To immediately build every change in one of the source files, use this command. 112 | The _core_ bundle will then be constantly updated in dist/bogJS-dev.js 113 | ```shell 114 | npm run watch 115 | 116 | --> dist/bogJS-dev.js 117 | ``` 118 | 119 | 120 | To build and bundle a new relese version of the project, use this command. The 121 | files will be created under "dist/": 122 | 123 | ```shell 124 | npm run build-release 125 | 126 | --> bogJS-vX.X.X.js 127 | --> bogJS-vX.X.X.js.map 128 | --> bogJS-vX.X.X.min.js 129 | --> bogJS-vX.X.X.min.js.map 130 | --> bogJS-ui-vX.X.X.js 131 | --> bogJS-ui-vX.X.X.js.map 132 | --> bogJS-ui-vX.X.X.min.js 133 | --> bogJS-ui-vX.X.X.min.js.map 134 | ``` 135 | Note: This command will also add a new Git tag with increased patch number and 136 | will also update the package.json file accordingly. To increase the version tag 137 | with a minor or even major, execute 138 | 139 | ``` 140 | npm version minor 141 | ``` 142 | 143 | or 144 | 145 | ``` 146 | npm version major 147 | ``` 148 | 149 | See also https://docs.npmjs.com/cli/version 150 | 151 | 152 | ## Documentation 153 | To create the documentation, you will need [jsdoc3](https://github.com/jsdoc3/jsdoc): 154 | ```shell 155 | jsdoc -c jsdoc.conf 156 | ``` 157 | The default folder for the generated documentation is doc/ . 158 | 159 | 160 | ## Version Info 161 | With every build process, the version info is included to the bundle and saved 162 | as global javascript variables 163 | ```javascript 164 | __BROWSERIFY_META_DATA__CREATED_AT 165 | // --> "Tue Feb 16 2016 21:13:38 GMT+0100 (CET)" 166 | 167 | __BROWSERIFY_META_DATA__GIT_VERSION 168 | // --> "1448b3e v0.2.8" 169 | ``` 170 | 171 | ## Basic concepts 172 | #### The scene file 173 | All object and scene relevant information is stored and read from a scene 174 | file. The format currently used is kind of a OSC string combined with 175 | [SpatDIF](http://www.spatdif.org/) commands. 176 | 177 | The SceneReader class can be replaced as long as the ObjectManager class 178 | instance retrieves the scene and object info as defined. See the documentation 179 | for further info. 180 | 181 | For the future, it is planned to implement a JSON representation of the ADM 182 | format. 183 | 184 | #### Implemented object descriptors 185 | The following parameters can be currently assigned to the objects: 186 | - Gain [float]: Values for the gain of the audio signal, connected to the 187 | PannerNode. Values must be between 0 and 1. 188 | - Position [float, cartesian, right-hand]: 189 | X, Y and Z values represent the position of the objects. See 190 | [here](https://webaudio.github.io/web-audio-api/#the-audiolistener-interface) 191 | for further info regarding the coordinate system. 192 | - Interactive [boolean]: This parameter is intended to be used if the object 193 | shall or shall not offer any interactive usage by the user. Example use case might be 194 | the adjustment of speech to music level. 195 | - Active [boolean]: This parameter can be used if the object is in the scene 196 | but should not be heard. It is kind of similar to the gain parameter. 197 | 198 | It is further planned to implement more sophisticated parameters of 199 | [ADM](https://www.itu.int/rec/R-REC-BS.2076/en) in the future. 200 | 201 | #### To load and play audio signals, you can use three different options: 202 | - Single audio objects (rather short signals up to a few minutes), starting 203 | with playback from the very beginning or at a dedicated playback time. These 204 | objects will be loaded via XMLHttpRequests and decoded with the Web Audio API 205 | built-in decodeAudioData() function. The representation of those signals will 206 | be a AudioBufferSouceNode. The bogJS wrapper for this is the AudioData 207 | class. A typical use case would be a effect or speech object. 208 | - Multiple single objects that have the identical duration and that shall be 209 | played back simultaneaously. For those objects, there is a dedicated class 210 | (IRTPlayer) which can or should be used for this purpose. The IRTPlayer 211 | class uses several AudioData instances to load and play files. 212 | - One or more audio objects with longer duration (> 3 minutes). These audio 213 | signals shall have the same duration as the audio scene. A typical usage 214 | would be one or multiple audio beds (containing e.g. atmo and music) that 215 | can have a file or stream representation. The HTML5 media element is 216 | used for this purpose and connected to the Web Audio API via a 217 | MediaElementSourceNode. The media file or stream can have multiple channels. 218 | Depending on the browser and codec, different maximum track numbers are 219 | possible. More than eight channels are currently only possible with a .wav 220 | container. See also [here](./tools/Multichannel-Order_Browsertest.xlsx). 221 | 222 | #### The channel order detection class 223 | If you use an audio bed for your scene, the ChannelOrderTest() class is used to 224 | detect the order of the decoded channels. To make use of this functionality, 225 | you have to encode [these](http://akamai-progressive.irt.de/demos/bogJS/channel_order/channelOrder_testset.zip) 226 | test files with your preferred encoder and settings. The test set contains 227 | uncompressed wav files from 2 channels up to 16 chs with a duration of 48000 228 | samples (1 second@48kHz). For each channel, a different sinus tone is used 229 | to detect the order after the encoding and decoding again. 230 | Further, you need to upload those files to a folder on the server where the 231 | HTML file using bogJS is located. Otherwise you will likely run into a CORS 232 | issue. 233 | For OS X user, there is a encoding bash script enclosed under /tools. 234 | 235 | ```javascript 236 | /* Simple example */ 237 | var ch = new ChannelOrderTest("mp4", 6); 238 | ch.testChs(); 239 | // -> [0, 2, 1, 4, 5, 3] 240 | 241 | /* Advanced example */ 242 | var ctx = new AudioContext(); 243 | var ch; 244 | $(ch).on('order_ready', function(e, order){ 245 | console.log("Got channel order: " + order); 246 | doSomething(); 247 | }); 248 | ch = new ChannelOrderTest("ogg", 4, ctx, "path/to/testfiles/"); 249 | // -> Got channel order: [0, 3, 2, 1] 250 | ``` 251 | 252 | #### Container and file extensions 253 | If no file extension is given in the scene file, the AudioData() class and 254 | the ObjectManager() class will firstly detect the capabilities of the used 255 | browser and then try to load the given file with the preferred extension. 256 | The order can be read and changed [here](./src/html5_player/core.js): 257 | 1. .mp4 258 | 2. .opus 259 | 3. .ogg 260 | 4. .mp3 261 | 5. .wav 262 | 263 | #### Interpolation 264 | Especially for object movements, we need interpolated position updates of the 265 | values. The Web Audio API offers with the [AudioParam](https://webaudio.github.io/web-audio-api/#AudioParam) 266 | interface the possibility to do this in certain ways. Unfortunately, the 267 | PannerNode (used for object positioning) does not support this interface. Only 268 | the new SpatialPannerNode will support this. Once the major browser support 269 | the SpatialPannerNode, it will be implemented here. As this might happen in the 270 | near future, I decided not to implement any interpolation logic as it will be 271 | integrated shortly anyway. Hence, you need to interpolate the object positions 272 | by yourself until this is fixed. 273 | 274 | For other object parameters (currently only Gain), the framework basically 275 | offers interpolation features, but I recommend not to mix them. 276 | 277 | #### Timing 278 | All time relevant changes (start of playback, position update, ..) is realized 279 | with the [WAAClock](https://github.com/sebpiq/WAAClock). Once the 280 | ObjectManager retrieved the scene data from the file, all necessary commands 281 | are registered with WAAClock events. This makes sure that critical timing 282 | updates are executed in time. 283 | 284 | #### File vs Stream 285 | Even though bogJS was designed to be capable of reading scene streams, it is 286 | currently not supported yet. All neccesarry scene data needs to be stored in a 287 | text file and passed to the ObjectManager instance. 288 | 289 | ## Advanced example 290 | This example uses audio beds over the entire duration, single objects and 291 | grouped objects with changing positions and other settings over time. 292 | 293 | ```spatdif 294 | /spatdif/meta/audiobed/url demos/5ch_bed_music1+2_atmo3+4_speech5 295 | /spatdif/meta/audiobed/tracks 5 296 | 297 | /spatdif/time 0.0 298 | /spatdif/source/Bed0/position -1.0 0.0 -1.0 299 | /spatdif/source/Bed0/active 1 300 | /spatdif/source/Bed0/track_mapping bed_0 301 | /spatdif/source/Bed0/gain 1 302 | /spatdif/source/Bed1/position 1.0 0.0 -1.0 303 | /spatdif/source/Bed1/active 1 304 | /spatdif/source/Bed1/track_mapping bed_1 305 | /spatdif/source/Bed1/gain 1 306 | /spatdif/source/Bed2/position -1.0 0.0 1.0 307 | /spatdif/source/Bed2/active 1 308 | /spatdif/source/Bed2/track_mapping bed_2 309 | /spatdif/source/Bed2/gain 1 310 | /spatdif/source/Bed3/position 1.0 0.0 1.0 311 | /spatdif/source/Bed3/active 1 312 | /spatdif/source/Bed3/track_mapping bed_3 313 | /spatdif/source/Bed3/gain 1 314 | /spatdif/source/Speech/position 0.05 0.0 -2.52 315 | /spatdif/source/Speech/active 1 316 | /spatdif/source/Speech/track_mapping bed_4 317 | /spatdif/source/Speech/gain 1 318 | 319 | /spatdif/time 70.754 320 | /spatdif/source/Birds1_L/position -0.86 0.0 -1.77 321 | /spatdif/source/Birds1_L/interpolate 0 322 | /spatdif/source/Birds1_L/active 1 323 | /spatdif/source/Birds1_L/track_mapping http://akamai-progressive.irt.de/demos/vulcano/Geflatter_1+3_L 324 | /spatdif/source/Birds1_L/gain 1 325 | /spatdif/source/Birds1_L/interactive 0 326 | /spatdif/source/Birds1_R/position 0.86 0.0 -1.79 327 | /spatdif/source/Birds1_R/interpolate 0 328 | /spatdif/source/Birds1_R/active 1 329 | /spatdif/source/Birds1_R/track_mapping http://akamai-progressive.irt.de/demos/vulcano/Geflatter_1+3_R 330 | /spatdif/source/Birds1_R/gain 1 331 | /spatdif/source/Birds1_R/interactive 0 332 | 333 | /spatdif/time 70.804 334 | /spatdif/source/Birds1_L/position -0.8605 0.0 -1.645 335 | /spatdif/source/Birds1_R/position 0.8595 0.0 -1.6645 336 | 337 | /spatdif/time 70.854 338 | /spatdif/source/Birds1_L/position -0.861 0.0 -1.52 339 | /spatdif/source/Birds1_R/position 0.859 0.0 -1.539 340 | 341 | /spatdif/time 117.55 342 | /spatdif/source/Stones_L/position -0.64 0.0 -0.79 343 | /spatdif/source/Stones_L/active 1 344 | /spatdif/source/Stones_L/track_mapping http://akamai-progressive.irt.de/demos/vulcano/Gesteinsbrocken_L 345 | /spatdif/source/Stones_L/gain 1 346 | /spatdif/source/Stones_L/interactive 0 347 | /spatdif/source/Stones_L/group Stones 348 | 349 | /spatdif/source/Stones_R/position 0.29 0.0 -0.88 350 | /spatdif/source/Stones_R/active 1 351 | /spatdif/source/Stones_R/track_mapping http://akamai-progressive.irt.de/demos/vulcano/Gesteinsbrocken_R 352 | /spatdif/source/Stones_R/gain 1 353 | /spatdif/source/Stones_R/interactive 0 354 | /spatdif/source/Stones_R/group Stones 355 | 356 | /spatdif/time 117.6 357 | /spatdif/source/Stones_L/position -0.6215 0.0 -0.7375 358 | /spatdif/source/Stones_R/position 0.2905 0.0 -0.8255 359 | /spatdif/source/Stones_R/gain 0.75 360 | 361 | /spatdif/time 120.0 362 | /spatdif/source/Stones_L/position -0.603 0.0 -0.685 363 | /spatdif/source/Stones_L/active 0 364 | /spatdif/source/Stones_R/position 0.291 0.0 -0.771 365 | /spatdif/source/Stones_R/gain 0.5 366 | 367 | /spatdif/time 123.8 368 | /spatdif/source/Stones_L/position -0.5845 0.0 -0.6325 369 | /spatdif/source/Stones_L/active 1 370 | /spatdif/source/Stones_R/position 0.2915 0.0 -0.7165 371 | /spatdif/source/Stones_R/gain 0.25 372 | ``` 373 | 374 | 375 | ## Authors 376 | Michael Weitnauer (), Michael Meier () 377 | 378 | ## Acknowledgement 379 | Parts of this framework were developed in the European collaborative research 380 | project [ORPHEUS](http://orpheus-audio.eu). This project has received funding 381 | from the European Union's Horizon 2020 research and innovation programme under 382 | grant agreement No 687645. 383 | Follow ORPHEUS on Twitter: [@ORPHEUS_AUDIO](https://twitter.com/ORPHEUS_AUDIO) 384 | 385 | ## License 386 | This framework is published under the [MIT](./LICENSE) License. 387 | 388 | ## 3rd Party Libraries used for this Project 389 | - [jQuery mousewheel](https://github.com/jquery/jquery-mousewheel) 390 | - [jQuery UI](https://github.com/jquery/jquery-ui) 391 | - [jQuery Transit](https://github.com/rstacruz/jquery.transit) 392 | - [loglevel](https://github.com/pimterry/loglevel) 393 | - [underscore.js](https://github.com/jashkenas/underscore) 394 | - [WAAClock.js](https://github.com/sebpiq/WAAClock) 395 | - [Bootstrap](https://github.com/twbs/bootstrap) 396 | -------------------------------------------------------------------------------- /css/images/head_logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/head_logo.gif -------------------------------------------------------------------------------- /css/images/irt_48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/irt_48x48.png -------------------------------------------------------------------------------- /css/images/ui-bg_diagonals-thick_18_b81900_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-bg_diagonals-thick_18_b81900_40x40.png -------------------------------------------------------------------------------- /css/images/ui-bg_diagonals-thick_20_666666_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-bg_diagonals-thick_20_666666_40x40.png -------------------------------------------------------------------------------- /css/images/ui-bg_flat_10_000000_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-bg_flat_10_000000_40x100.png -------------------------------------------------------------------------------- /css/images/ui-bg_glass_100_f6f6f6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-bg_glass_100_f6f6f6_1x400.png -------------------------------------------------------------------------------- /css/images/ui-bg_glass_100_fdf5ce_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-bg_glass_100_fdf5ce_1x400.png -------------------------------------------------------------------------------- /css/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /css/images/ui-bg_gloss-wave_35_f6a828_500x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-bg_gloss-wave_35_f6a828_500x100.png -------------------------------------------------------------------------------- /css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png -------------------------------------------------------------------------------- /css/images/ui-bg_highlight-soft_75_ffe45c_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-bg_highlight-soft_75_ffe45c_1x100.png -------------------------------------------------------------------------------- /css/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /css/images/ui-icons_228ef1_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-icons_228ef1_256x240.png -------------------------------------------------------------------------------- /css/images/ui-icons_ef8c08_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-icons_ef8c08_256x240.png -------------------------------------------------------------------------------- /css/images/ui-icons_ffd27a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-icons_ffd27a_256x240.png -------------------------------------------------------------------------------- /css/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/css/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /css/jquery-ui.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.11.4 - 2015-07-16 2 | * http://jqueryui.com 3 | * Includes: core.css, draggable.css, resizable.css, selectable.css, sortable.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, menu.css, progressbar.css, selectmenu.css, slider.css, spinner.css, tabs.css, tooltip.css, theme.css 4 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px 5 | * Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ 6 | 7 | .ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;min-height:0;font-size:100%}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:none}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{position:relative;margin:0;padding:3px 1em 3px .4em;cursor:pointer;min-height:0;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-button{display:inline-block;overflow:hidden;position:relative;text-decoration:none;cursor:pointer}.ui-selectmenu-button span.ui-icon{right:0.5em;left:auto;margin-top:-8px;position:absolute;top:50%}.ui-selectmenu-button span.ui-selectmenu-text{text-align:left;padding:0.4em 2.1em 0.4em 1em;display:block;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#eee url("images/ui-bg_highlight-soft_100_eeeeee_1x100.png") 50% top repeat-x;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #e78f08;background:#f6a828 url("images/ui-bg_gloss-wave_35_f6a828_500x100.png") 50% 50% repeat-x;color:#fff;font-weight:bold}.ui-widget-header a{color:#fff}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #ccc;background:#f6f6f6 url("images/ui-bg_glass_100_f6f6f6_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#1c94c4}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#1c94c4;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #fbcb09;background:#fdf5ce url("images/ui-bg_glass_100_fdf5ce_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#c77405}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#c77405;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #fbd850;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#eb8f00}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#eb8f00;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fed22f;background:#ffe45c url("images/ui-bg_highlight-soft_75_ffe45c_1x100.png") 50% top repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#b81900 url("images/ui-bg_diagonals-thick_18_b81900_40x40.png") 50% 50% repeat;color:#fff}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#fff}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#fff}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_228ef1_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_ffd27a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#666 url("images/ui-bg_diagonals-thick_20_666666_40x40.png") 50% 50% repeat;opacity:.5;filter:Alpha(Opacity=50)}.ui-widget-shadow{margin:-5px 0 0 -5px;padding:5px;background:#000 url("images/ui-bg_flat_10_000000_40x100.png") 50% 50% repeat-x;opacity:.2;filter:Alpha(Opacity=20);border-radius:5px} -------------------------------------------------------------------------------- /css/logo-nav.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - Logo Nav HTML Template (http://startbootstrap.com) 3 | * Code licensed under the Apache License v2.0. 4 | * For details, see http://www.apache.org/licenses/LICENSE-2.0. 5 | */ 6 | 7 | body { 8 | padding-top: 70px; /* Required padding for .navbar-fixed-top. Change if height of navigation changes. */ 9 | } 10 | 11 | .navbar-fixed-top .nav { 12 | padding: 15px 0; 13 | } 14 | 15 | .navbar-fixed-top .navbar-brand { 16 | padding: 0 15px; 17 | } 18 | 19 | @media(min-width:768px) { 20 | body { 21 | padding-top: 100px; /* Required padding for .navbar-fixed-top. Change if height of navigation changes. */ 22 | } 23 | 24 | .navbar-fixed-top .navbar-brand { 25 | padding: 15px 0; 26 | } 27 | } -------------------------------------------------------------------------------- /css/ui.css: -------------------------------------------------------------------------------- 1 | .OM_area { 2 | background: #fbf8e9; 3 | border-color: #f9f3d9; 4 | color: #4d4e53; 5 | border-width: 5px; 6 | border-style: solid; 7 | padding: 10px; 8 | margin: 20px auto; 9 | width: 500px; 10 | height: 500px; 11 | position: relative; 12 | } 13 | 14 | .irt_listener{ 15 | background-image: url("../img/listener.png"); 16 | width: 32px; 17 | height: 32px; 18 | position: absolute; 19 | /* 20 | margin-left: auto; 21 | margin-top: 50%; 22 | */ 23 | } 24 | 25 | 26 | .irt_object{ 27 | background-image: url("../img/ls.png"); 28 | width: 32px; 29 | height: 32px; 30 | position: absolute; 31 | } 32 | 33 | .irt_object_disabled{ 34 | background-image: url("../img/ls_disabled.png"); 35 | width: 32px; 36 | height: 32px; 37 | position: absolute; 38 | } 39 | 40 | .irt_object:hover{ 41 | cursor: move; 42 | } 43 | 44 | .irt_object_title { 45 | margin-top: 32px; 46 | /*color: #2D4E0D;*/ 47 | font-family: sans-serif; 48 | font-size: smaller; 49 | } 50 | 51 | .irt_switchPanningMode{ 52 | width: 32px; 53 | height: 32px; 54 | position: absolute; 55 | bottom: 5px; 56 | right: 5px; 57 | } 58 | 59 | .irt_resetOrientation{ 60 | width: 32px; 61 | height: 32px; 62 | position: absolute; 63 | bottom: 5px; 64 | right: 74px; 65 | } 66 | 67 | .irt_resetOrientation:hover{ 68 | cursor: pointer; 69 | } 70 | 71 | .irt_switchPanningMode:hover{ 72 | cursor: pointer; 73 | } 74 | 75 | .irt_toggleInteractiveMode{ 76 | width: 36px; 77 | height: 32px; 78 | position: absolute; 79 | bottom: 5px; 80 | right: 37px; 81 | } 82 | 83 | .irt_toggleInteractiveMode:hover{ 84 | cursor: pointer; 85 | } 86 | 87 | .irt_msg{ 88 | position: relative; 89 | padding: 20px; 90 | width: 350px; 91 | margin: 0px auto; 92 | text-align: center; 93 | font-family: sans-serif; 94 | font-size: large; 95 | } 96 | 97 | .irt_section { 98 | /* 99 | padding-top: 40px; 100 | margin-top: -40px; 101 | */ 102 | } 103 | 104 | .anchor_section { 105 | display: block; 106 | position: relative; 107 | top: -70px; 108 | visibility: hidden; 109 | } 110 | 111 | .irt_btn_basic { 112 | position: relative; 113 | z-index: 10; 114 | float: left; 115 | width: 60px; 116 | height: 34px; 117 | margin-right: 10px; 118 | background-repeat: no-repeat; 119 | background-position: center; 120 | background-image: url("../img/play_disabled.png"); 121 | } 122 | 123 | .btn_start { 124 | background-image: url("../img/play.png"); 125 | } 126 | 127 | .btn_pause { 128 | background-image: url("../img/pause.png"); 129 | } 130 | 131 | .btn_stop { 132 | background-image: url("../img/stop.png"); 133 | } 134 | 135 | .btn_stop_disabled { 136 | background-image: url("../img/stop_disabled.png"); 137 | } 138 | -------------------------------------------------------------------------------- /img/control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/control.png -------------------------------------------------------------------------------- /img/control_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/control_disabled.png -------------------------------------------------------------------------------- /img/headphones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/headphones.png -------------------------------------------------------------------------------- /img/headphones_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/headphones_disabled.png -------------------------------------------------------------------------------- /img/listener.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/listener.png -------------------------------------------------------------------------------- /img/ls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/ls.png -------------------------------------------------------------------------------- /img/ls_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/ls_disabled.png -------------------------------------------------------------------------------- /img/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/pause.png -------------------------------------------------------------------------------- /img/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/play.png -------------------------------------------------------------------------------- /img/play_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/play_disabled.png -------------------------------------------------------------------------------- /img/stereo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/stereo.png -------------------------------------------------------------------------------- /img/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/stop.png -------------------------------------------------------------------------------- /img/stop_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/img/stop_disabled.png -------------------------------------------------------------------------------- /index-ui.js: -------------------------------------------------------------------------------- 1 | 2 | // making the objects globally available 3 | window.UIManager = require('./src/ui'); 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | bogJS - Object-based audio 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 72 | 73 | 74 | 75 |
76 |
77 |
78 |

Object-based audio

79 |

80 | Object-based audio is a revolutionary approach for creating and 81 | deploying interactive, personalised, scalable and immersive 82 | content, by representing it as a set of individual assets 83 | together with metadata describing their relationships and 84 | associations. 85 | This allows media objects to be assembled in ground-breaking ways to 86 | create new user experiences. With the introduction of HTML5 87 | and the Web Audio API, an important prerequisite was made for 88 | native rendering of object-based audio in modern browsers. 89 | 90 |

91 |
92 |
93 |
94 | 95 | 96 | 97 | 98 |
99 |
100 |
101 |

How to use this demo

102 |
103 |
104 |

Technical requirements

105 |

106 | To play this demo, you need a modern browser which supports 107 | HTML5 and the Web Audio API. This is the case for all major 108 | browsers (Firefox, Chrome, Safari and Opera). Only Internet 109 | Explorer supports the Web Audio API only in Edge with Windows 110 | 10. You can check the support by visiting 111 | this page. 112 |

113 |

Starting the demo

114 |

115 | To start the demo, choose a scene from the list and press the play 116 | button left of it once the audio files are loaded. Depending 117 | on the capabilities of your browser, either m4a, ogg or mp3 118 | files will be downloaded. If your internet connection is rather slow 119 | and / or the audio files are rather large, it might take a while 120 | until you can start the demo. 121 |

122 |

Choosing the rendering mode

123 |

124 | You can choose between simply Stereo rendering (default) and 125 | fancy headphone rendering by clicking the icon in the lower right 126 | corner. 127 |
128 | Note: If you are using the Internet Explorer "Edge", only 129 | Stereo rendering is supported currently. 130 |

131 |
132 |
133 |

Moving the listener

134 |

135 | You can rotate the listener's orientation by positioning the 136 | mouse above the listener's icon and rotate the mousewheel up 137 | or down. 138 |
139 | One can also change the position of the listener within the 140 | scene by moving it while pressing the mouse button. 141 |

142 |

Object interaction

143 |

144 | By clicking the controls icon left of the rendering mode icon, 145 | you can acticate the interactve mode. Any scene commands will 146 | be ignored and you can change the position of objects. 147 |
148 | Furthermore, you can mute / unmute objects by double clicking 149 | them. 150 |

151 |
152 |
153 |
154 | 155 | 156 | 157 | 158 |
159 |
160 |
161 |

Demo

162 |

163 |

164 |
165 | 166 | 174 |
175 | 176 |
177 |
178 |
179 |

No scene selected

180 |
181 | 186 |
187 |
188 | 189 | 190 |
191 | 192 | 193 |
194 |
195 | 196 | 197 |
198 |
199 |
200 | 259 |
260 |
261 |
262 | 263 | 264 |
265 | 266 | 267 | 268 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | // making the objects globally available 3 | window.ChannelOrderTest = require('./src/channelorder_test'); 4 | window.AudioData = require('./src/html5_player/core').AudioData; 5 | window.IRTPlayer = require('./src/html5_player/core').IRTPlayer; 6 | window.GainController = require('./src/gain_controller'); 7 | window.MediaElementController = require('./src/media_controller'); 8 | window.ObjectController = require('./src/object'); 9 | window.ObjectManager = require('./src/object_manager'); 10 | window.SceneReader = require('./src/scene_reader'); 11 | //window.UIManager = require('./src/ui'); 12 | -------------------------------------------------------------------------------- /jsdoc.conf: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": ["src/ui.js", 4 | "src/object.js", 5 | "src/object_manager.js", 6 | "src/scene_reader.js", 7 | "src/channelorder_test.js", 8 | "src/media_controller.js", 9 | "src/html5_player/core.js"] 10 | }, 11 | 12 | "opts": { 13 | "template": "templates/default", // same as -t templates/default 14 | "encoding": "utf8", // same as -e utf8 15 | "destination": "./doc/", // same as -d ./out/ 16 | "recurse": true, // same as -r 17 | "readme": "README.md" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bogJS", 3 | "version": "0.4.5", 4 | "description": "A JS framework for object-based rendering in browsers", 5 | "main": "index.js", 6 | "scripts": { 7 | "build-debug": "simplifyify index.js -o dist/bogJS-latest.js --debug --bundle", 8 | "build-debug-ui": "simplifyify index-ui.js -o dist/bogJS-ui-latest.js --debug --bundle", 9 | "build-min": "simplifyify index.js -o dist/bogJS-latest.min.js --minify --debug", 10 | "build-min-ui": "simplifyify index-ui.js -o dist/bogJS-ui-latest.min.js --minify --debug", 11 | "build": "npm run build-debug && npm run build-min && npm run build-debug-ui && npm run build-min-ui", 12 | "build-release": "npm version patch && simplifyify index.js -o dist/bogJS-`git describe --abbrev=0`.js --minify --debug --bundle && simplifyify index-ui.js -o dist/bogJS-ui-`git describe --abbrev=0`.js --minify --debug --bundle && npm run build", 13 | "watch": "simplifyify index.js -o dist/bogJS-dev.js --watch --bundle --debug" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git@gitlab.irt.de:pa/bogJS.git" 18 | }, 19 | "browserify": { 20 | "transform": [ 21 | "commitify", 22 | "deamdify", 23 | [ 24 | "babelify", 25 | { 26 | "presets": [ 27 | "es2015" 28 | ] 29 | } 30 | ] 31 | ] 32 | }, 33 | "author": "Michael Weitnauer", 34 | "license": "MIT", 35 | "dependencies": { 36 | "jquery-mousewheel": "^3.1.13", 37 | "jquery-ui-browserify": "^1.11.0-pre-seelio", 38 | "jquery.transit": "^0.9.12", 39 | "underscore": "^1.9.1", 40 | "waaclock": "^0.5.3" 41 | }, 42 | "devDependencies": { 43 | "babel-preset-es2015": "^6.24.1", 44 | "babelify": "^7.3.0", 45 | "browserify": "^14.5.0", 46 | "commitify": "git://github.com/kickermeister/commitify.git#master", 47 | "deamdify": "^0.2.0", 48 | "jquery": "^3.5.0", 49 | "jsdoc": "^3.6.3", 50 | "simplifyify": "^7.0.5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/channelorder_test.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | /** 3 | * @file channelorder_test.js 4 | * @author Michael Weitnauer: {@link weitnauer@irt.de} 5 | */ 6 | 7 | /** 8 | * @module bogJS 9 | */ 10 | 11 | 12 | var _ = require('underscore'); 13 | 14 | 15 | /** 16 | * GainController 17 | * @constructor 18 | * 19 | * @param ctx - Web Audio API Audio Context instance 20 | * @param [targetNode=ctx.destination] - Web Audio API node to which the 21 | * output of the GainController shall be connected to. 22 | */ 23 | 24 | /** 25 | * ChannelOrderTest will start loading, deconding and playing as soon as the 26 | * instance of the class is created. The test files will be looped and for 27 | * each loop, the [testChs]{@link module:bogJS~ChannelOrderTest#testChs} method 28 | * is called. If the test file has been played five times and no order could 29 | * be detected, the default order will be triggered. 30 | * @constructor 31 | * 32 | * @param {String} container - to be tested file extension w/o dot ("mp4") 33 | * @param {Number} tracks - To be tested channel number for container 34 | * @param {Object.} [ctx=AudioContext] - if no AudioContext 35 | * instance is passed, it will be created. 36 | * @param {String} [root="signals/order"] - path to test encoded files 37 | * @fires module:bogJS~ChannelOrderTest#order_ready 38 | */ 39 | var ChannelOrderTest = function(container, tracks, ctx, root="signals/order/"){ 40 | if (typeof ctx === 'undefined') { 41 | if (typeof AudioContext !== 'undefined') { 42 | var ctx = new AudioContext(); 43 | } else if (typeof webkitAudioContext !== 'undefined') { 44 | var ctx = new webkitAudioContext(); 45 | } else { 46 | alert("Your browser doesn't support the Web Audio API!"); 47 | } 48 | } 49 | /** @var {Object.} */ 50 | this.ctx = ctx; 51 | 52 | this._tracks = parseInt(tracks); 53 | this._splitter = this.ctx.createChannelSplitter(this._tracks); 54 | this.analysers = []; 55 | this.gainNode = this.ctx.createGain(); 56 | this.gainNode.gain.value = 0; 57 | this.gainNode.connect(this.ctx.destination); 58 | 59 | for (var i = 0; i < this._tracks; i++){ 60 | this.analysers[i] = this.ctx.createAnalyser(); 61 | this.analysers[i].fftSize = 2048; // "hard-coded" due to Safari -> analyser chrashes if fftSize value is greater than 2048 62 | this._splitter.connect(this.analysers[i], i); 63 | this.analysers[i].connect(this.gainNode); 64 | } 65 | //var root = root || "http://lab.irt.de/demos/order/"; 66 | if (container === "webm"){ // we assume opus if webm is used 67 | container = "opus"; 68 | } 69 | var url = root+tracks+"chs."+container; 70 | this._loadSound(url); 71 | }; 72 | 73 | 74 | ChannelOrderTest.prototype = { 75 | /** 76 | * Load and test passed audio signal 77 | * 78 | * @protected 79 | * @param {string} url - URL 80 | */ 81 | _loadSound: function(url){ 82 | this.audio = document.createElement('audio'); 83 | this.audio.src = url; 84 | this.audio.loop = true; 85 | this.audio.load(); 86 | this.mediaElement = this.ctx.createMediaElementSource(this.audio); 87 | this.mediaElement.connect(this._splitter); 88 | this.audio.play(); 89 | var last_unique = []; 90 | 91 | this.audio.onended = function(){ 92 | console.debug("ChannelOrderTest Playback ended"); 93 | } 94 | 95 | // onplay will be fired once the audio playback started 96 | $(this.audio).on("play", function(){ 97 | console.debug("Channel order testfile started..."); 98 | // this is a fix to make the channel order test working on Firefox 99 | // the initial attempt (listen on "playing") did no more in FF after 100 | // an update. 101 | for (let i = 0, p = Promise.resolve(); i < 10; i++) { 102 | p = p.then(() => new Promise(resolve => 103 | setTimeout(function () { 104 | var order = this.testChs(); 105 | var unique = _.unique(order); 106 | // the returned order should be identical for two consecutive calls 107 | // to make sure we have a reliable result 108 | if ((unique.length === this._tracks) && (_.isEqual(last_unique, unique))) { 109 | console.info('Channel order detected: ' + order); 110 | /** 111 | * If channel order was detected and ensured, the event is 112 | * fired with channel order as array. 113 | * @event module:bogJS~ChannelOrderTest#order_ready 114 | * @property {Number[]} order - Array containing the detected 115 | * order 116 | */ 117 | $(document).triggerHandler('order_ready', [order]); 118 | this.audio.pause(); 119 | return; 120 | } else if (unique.length === this._tracks){ 121 | last_unique = unique; 122 | } 123 | console.debug("Channel order not yet detected. Iteration: " + i); 124 | if (i >= 9){ 125 | console.warn("Channel order not detectable. Stopping indentfication and trigger default values."); 126 | order = _.range(this._tracks); 127 | $(document).triggerHandler('order_ready', [order]); 128 | this.audio.pause(); 129 | } 130 | resolve(); 131 | }.bind(this), 500) 132 | )); 133 | } 134 | }.bind(this, last_unique)); 135 | }, 136 | 137 | /** 138 | * Save frequency bins to arrays for later analysis 139 | * @protected 140 | * @returns {Number[]} Nested array (Float32Array) containing the frequency 141 | * bins for each channel 142 | */ 143 | _getFreqData: function(){ 144 | var freqBins = []; 145 | var freqBinaryBins = []; 146 | for (var i = 0; i < this._tracks; i++){ 147 | // Float32Array should be the same length as the frequencyBinCount 148 | freqBins[i] = new Float32Array(this.analysers[i].frequencyBinCount); 149 | // fill the Float32Array with data returned from getFloatFrequencyData() 150 | this.analysers[i].getFloatFrequencyData(freqBins[i]); 151 | } 152 | return freqBins; 153 | }, 154 | 155 | /** 156 | * Will conduct the detection of the channel order. 157 | * @returns {Number[]} Array containing the detected. e.g. [0, 3, 1, 2] 158 | * channel order 159 | */ 160 | testChs: function(){ 161 | var freqBins = this._getFreqData(); 162 | var indices = []; 163 | for (var i = 0; i < freqBins.length; i++){ 164 | var idx = _.indexOf(freqBins[i], _.max(freqBins[i])); 165 | indices[i] = idx; 166 | } 167 | console.debug("Decoded indices: " + indices); 168 | // to avoid the array is mutated and numerical sorted 169 | var sorted_indices = indices.concat().sort(function(a, b){return a-b;}); 170 | console.debug("Sorted indices: " + sorted_indices); 171 | var normalized_indices = []; 172 | for (var i = 0; i < indices.length; i++){ 173 | normalized_indices[i] = _.indexOf(sorted_indices, indices[i]); 174 | } 175 | return normalized_indices; 176 | }, 177 | 178 | /** 179 | * Explicit play function for mobile devices which will not start the media 180 | * element automatically without user gesture. 181 | */ 182 | playAudio: function(){ 183 | this.audio.play(); 184 | } 185 | }; 186 | 187 | module.exports = ChannelOrderTest; 188 | -------------------------------------------------------------------------------- /src/gain_controller.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | /** 3 | * @file media_controller.js 4 | * @author Michael Weitnauer: {@link weitnauer@irt.de} 5 | */ 6 | 7 | /** 8 | * @module bogJS 9 | * 10 | */ 11 | 12 | /** 13 | * GainController 14 | * @constructor 15 | * 16 | * @param ctx - Web Audio API Audio Context instance 17 | * @param [targetNode=ctx.destination] - Web Audio API node to which the 18 | * output of the GainController shall be connected to. 19 | */ 20 | var GainController = function(ctx, targetNode=ctx.destination){ 21 | this._gain = 1; 22 | this.gainNode = ctx.createGain(); 23 | 24 | // Experimental highpass to avoid sizzling noinse while chaning view / angle 25 | //this.highpass = ctx.createBiquadFilter(); 26 | //this.highpass.type = "highpass"; 27 | //this.highpass.connect(this.gainNode); 28 | //this.setHighpassFreq(80); 29 | 30 | // FIXME: if applied here, the gainNode stays 31 | // connected with ctx.destination: 32 | this.connect(targetNode); 33 | }; 34 | 35 | GainController.prototype = { 36 | 37 | /** 38 | * Mutes the node object 39 | * 40 | */ 41 | mute: function(){ 42 | this.setGain(0); 43 | }, 44 | 45 | /** 46 | * Unmutes node object 47 | * 48 | */ 49 | unmute: function(){ 50 | this.setGain(1); 51 | }, 52 | 53 | /** 54 | * setGain 55 | * 56 | * @param {Float} val - Values between 0 and 1 57 | */ 58 | setGain: function(val){ 59 | this.gainNode.gain.value = val; 60 | this._gain = this.getGain(); 61 | }, 62 | 63 | /** 64 | * getGain 65 | * 66 | * @returns {Float} gain - Float value between 0 and 1 67 | */ 68 | getGain: function(){ 69 | return this.gainNode.gain.value; 70 | }, 71 | 72 | /** 73 | * Disconnects and reconnects {@link GainController} instance to passed 74 | * AudioNode(s) 75 | * 76 | * @param {(Object|Object[])} nodes - Single of array of AudioNodes to which 77 | * the {@link MediaElementController} instance shall be reconnected. 78 | */ 79 | reconnect: function(nodes){ 80 | this.disconnect(); 81 | this.connect(nodes); 82 | }, 83 | 84 | /** 85 | * connect 86 | * 87 | * @param {(Object|Object[])} nodes - one or multple Web Audio API nodes to 88 | * which the output of the GainController instance shall be connected to. 89 | */ 90 | connect: function(nodes) { 91 | console.debug("Connecting GainController to " + nodes); 92 | if (Object.prototype.toString.call(nodes) != "[object Array]"){ // == single Node 93 | this.gainNode.connect(nodes); 94 | } else { // == array of Nodes 95 | for (var i=0; i < nodes.length; i++){ 96 | this.gainNode.connect(nodes[i]); 97 | } 98 | } 99 | }, 100 | 101 | /** 102 | * This method will disconnect output of the {@link GainController} instance from 103 | * a given node or all connected nodes if node is not given/undefined. 104 | */ 105 | disconnect: function(node){ 106 | //console.debug("Disconnecting ", this, " from ", node); 107 | this.gainNode.disconnect(node); 108 | }, 109 | 110 | setHighpassFreq: function(freq){ 111 | //this.highpass.frequency.value = freq; 112 | } 113 | }; 114 | 115 | module.exports = GainController; 116 | -------------------------------------------------------------------------------- /src/html5_player/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file irtPlayer_new.js 3 | * @author Michael Weitnauer: {@link weitnauer@irt.de} 4 | */ 5 | 6 | /** 7 | * @license 8 | * ---------------------------------------------------------------------------- 9 | * irtPlayer, a Javascript HTML5 Audio library for comparing audio files gaplessly 10 | * v2.0.0 11 | * Licensed under the MIT license. 12 | * http://www.irt.de 13 | * ---------------------------------------------------------------------------- 14 | * Copyright (C) 2015 Institut für Rundfunktechnik GmbH 15 | * http://www.irt.de 16 | * ---------------------------------------------------------------------------- 17 | * Permission is hereby granted, free of charge, to any person obtaining a copy 18 | * of this software and associated documentation files ( the "Software" ), to deal 19 | * in the Software without restriction, including without limitation the rights 20 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | * copies of the Software, and to permit persons to whom the Software is 22 | * furnished to do so, subject to the following conditions: 23 | * 24 | * The above copyright notice and this permission notice shall be included in 25 | * all copies or substantial portions of the Software. 26 | * 27 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 33 | * THE SOFTWARE. 34 | * ---------------------------------------------------------------------------- 35 | */ 36 | 37 | /** 38 | * @module irtPlayer 39 | * 40 | */ 41 | 42 | 43 | /** 44 | * Represents AudioData class which has all the logic to control an 45 | * audio signal 46 | * 47 | * @constructor 48 | * 49 | * @param {Object} ctx - An AudioContext instance. 50 | * @param {string} url - URL of the audio source (with or without 51 | * extension). 52 | * @param {Object} [targetNode=ctx.destination] - The audio node to which the AudioData 53 | * instance shall be connected 54 | * @param {boolean} [checkSupportFlag=true] - Enable / disable extension 55 | * support for passed url (see [AudioData._checkExtension]{@link AudioData#_checkExtension}) 56 | * 57 | * @fires module:irtPlayer~AudioData#audio_init 58 | * @fires module:irtPlayer~AudioData#audio_loaded 59 | * @fires module:irtPlayer~AudioData#audio_ended 60 | */ 61 | var AudioData = function(ctx, url, targetNode, checkSupportFlag) { 62 | /** @protected 63 | * @var {boolean} */ 64 | this.canplay = false; 65 | var checkSupportFlag = checkSupportFlag || true; 66 | if (checkSupportFlag == true){ 67 | var url = this._checkExtension(url); 68 | } 69 | /** @var {Object.} */ 70 | this.ctx = ctx; 71 | this.url = url; 72 | 73 | this._playing = false; 74 | this._looping = true; 75 | this._rangeStart = 0; 76 | this._rangeEnd = 0; 77 | this._startTime = 0; 78 | this._startOffset = 0; 79 | 80 | /** @var {Object.} */ 81 | this.gainNode = this.ctx.createGain(); 82 | this.gain = this.getGain(); 83 | var targetNode = targetNode || this.ctx.destination; 84 | this.gainNode.connect(targetNode); // FF either refuses to break this connection or simply displays a no more existing connection.. 85 | } 86 | 87 | AudioData.prototype = { 88 | 89 | /** 90 | * Create instance of new AudioBufferSource every time {@link 91 | * AudioData#play} is called and initialize it. 92 | * 93 | * @protected 94 | */ 95 | _initBuffer: function(){ 96 | this.audio = this.ctx.createBufferSource(); 97 | this.audio.loop = this._looping; 98 | //this.audio.loop = false; // workaround to compensate Chrome behavior. see comment in play() 99 | this.audio.buffer = this._buffer; 100 | this.audio.connect(this.gainNode); 101 | this.audio.loopStart = this._rangeStart; 102 | this.audio.loopEnd = this._rangeEnd; 103 | this.audio.onended = this._onendedHandler.bind(this); 104 | 105 | /** 106 | * Will be fired once the new AudioBufferSource has been 107 | * initilized. 108 | * @event module:irtPlayer~AudioData#audio_init 109 | */ 110 | $(this).triggerHandler("audio_init"); 111 | }, 112 | 113 | /** 114 | * Will be called if AudioBufferSource instance has ended 115 | * 116 | * @protected 117 | */ 118 | _onendedHandler: function(){ 119 | //console.debug("Audio buffer has ended!"); 120 | this._playing = false; 121 | //this._startOffset = 0; 122 | 123 | /** 124 | * Will be fired once the playback has ended 125 | * @event module:irtPlayer~AudioData#audio_ended 126 | */ 127 | $(this).triggerHandler("audio_ended"); 128 | }, 129 | 130 | load: function(){ 131 | this._loadSound(this.url); 132 | }, 133 | 134 | /** 135 | * Start playback of audio signal 136 | * 137 | * @param {number} [pos] - Position from which the playback shall start 138 | * (optional) 139 | */ 140 | play: function(pos){ 141 | if ((this._playing == false) && (this.canplay)){ 142 | this._initBuffer(); 143 | this._startTime = this.audio.context.currentTime; 144 | console.debug("Start time: " + this._startTime); 145 | if (typeof pos != 'number'){ // detection with _.isNumber() could be more robust 146 | var buffer_duration = this._buffer.duration; 147 | var offset = (this._rangeStart + this._startOffset) % buffer_duration; 148 | var duration = this._rangeEnd - offset; 149 | console.debug("Offset: " + offset + " Duration: " + duration); 150 | 151 | // Passing a duration to start() causes undefined 152 | // situation in current versions of Chrome. FF, Safari 153 | // and Opera seem to treat this situation properly. See 154 | // also https://github.com/WebAudio/web-audio-api/issues/421 155 | this.audio.start(0, offset, duration); 156 | //this.audio.start(0, offset); 157 | } else { 158 | console.debug("Starting playback at " + pos); 159 | this._startOffset = pos; 160 | var duration = this._rangeEnd - pos; 161 | this.audio.start(0, pos, duration); 162 | } 163 | // workaround to force looping in Chrome. see comment above. 164 | // Chrome seems to ignore looping state if duration is 165 | // passed. --> init() with loop = false, then set "real" 166 | // loop state here: 167 | //this.audio.loop = this._looping; 168 | this._playing = true; 169 | } 170 | }, 171 | 172 | /** 173 | * Pause playback - will only be executed if {@link 174 | * AudioData#_playing} flag is true. 175 | * 176 | */ 177 | pause: function(){ 178 | if (this._playing == true){ 179 | this.audio.stop(0); 180 | // Measure how much time passed since the last pause. 181 | this._startOffset += this.audio.context.currentTime - this._startTime; 182 | this._playing = false; 183 | console.debug("Start offset: "+ this._startOffset); 184 | } 185 | }, 186 | 187 | 188 | /** 189 | * Stops playback - if method is called during the playback 190 | * is stopped, the thrown error will be catched. 191 | */ 192 | stop: function(){ 193 | try { 194 | this.audio.stop(0); 195 | this._startOffset = 0; 196 | this._playing = false; 197 | } catch (err) { 198 | console.warn("Can't stop audio.. " + err); 199 | } 200 | }, 201 | 202 | /** 203 | * Sets gain of {@link AudioData} instance 204 | * 205 | * @param {float} gain - Value between 0.0 and 1.0 206 | */ 207 | setGain: function(gain){ 208 | if ((gain >= 0.0) && (gain <= 1.0)){ 209 | this.gainNode.gain.value = gain; 210 | this.gain = this.gainNode.gain.value; // avoids that we accept uncompatible values 211 | } 212 | else { 213 | console.warn("Gain values must be between 0 and 1"); 214 | } 215 | }, 216 | 217 | /** 218 | * Returns current gain value of {@link AudioData} instance 219 | * 220 | * @return {float} value - Float gain value 221 | */ 222 | getGain: function(){ 223 | return this.gainNode.gain.value; // or do we trust in this.gain ?? 224 | }, 225 | 226 | /** 227 | * Disables / enables the loop of the {@link AudioData} instance 228 | */ 229 | toggleLoop: function() { 230 | if (this._looping == false){ 231 | this._looping = true; 232 | } else { 233 | this._looping = false; 234 | } 235 | try { 236 | //this.pause(); 237 | this.audio.loop = this._looping; 238 | //this.play(); 239 | } catch (err) { 240 | console.warn("Can't set loop state: " + err); 241 | } 242 | }, 243 | 244 | /** 245 | * Disables / enables the loop of the {@link AudioData} instance 246 | */ 247 | setLoopState: function(bool) { 248 | this._looping = bool; 249 | try { 250 | //this.pause(); 251 | this.audio.loop = this._looping; 252 | //this.play(); 253 | } catch (err) { 254 | console.warn("Can't set loop state: " + err); 255 | } 256 | }, 257 | 258 | /** 259 | * Sets start position for playback 260 | * 261 | * @param {float} pos - Start playback always at passed 262 | * position 263 | */ 264 | setRangeStart: function(pos){ 265 | pos = parseFloat(pos); 266 | if (pos >= 0) { 267 | pos = pos; 268 | } else { 269 | pos = 0; 270 | } 271 | this._rangeStart = pos; 272 | try { 273 | this.audio.loopStart = this._rangeStart; 274 | console.debug("Loop start: " + pos); 275 | } catch (err) { 276 | console.warn("Can't set loop start yet.." + err); 277 | } 278 | }, 279 | 280 | /** 281 | * Sets end position for playback 282 | * 283 | * @param {float} pos - Playback end always at passed 284 | * position 285 | */ 286 | setRangeEnd: function(pos){ 287 | pos = parseFloat(pos); 288 | if (pos <= this._buffer.duration) { 289 | pos = pos; 290 | } else { 291 | pos = this._buffer.duration; 292 | } 293 | this._rangeEnd = pos; 294 | try { 295 | this.audio.loopEnd = this._rangeEnd; 296 | console.debug("Loop end: " + pos); 297 | } catch (err){ 298 | console.warn("Can't set loop start yet.." + err); 299 | } 300 | }, 301 | 302 | /** 303 | * Mutes {@link AudioData} instance 304 | */ 305 | mute: function(){ 306 | this.setGain(0.0); 307 | }, 308 | 309 | /** 310 | * Unmutes {@link AudioData} instance 311 | */ 312 | unmute: function(){ 313 | this.setGain(1.0); 314 | }, 315 | 316 | /** 317 | * Jump to passed position during playback 318 | * 319 | * @param {float} pos - Must be between 0 and {@link 320 | * AudioData._rangeEnd} 321 | */ 322 | setTime: function(pos){ 323 | if ((pos >= 0) && (pos <= this._rangeEnd)){ 324 | this.stop(); 325 | this.play(pos); 326 | } 327 | }, 328 | 329 | /** 330 | * Returns current playback position 331 | * 332 | * @return {number} value - Current playback position 333 | */ 334 | getTime: function(){ 335 | if (this._playing) { 336 | return this.audio.context.currentTime - this._startTime + this._startOffset; 337 | } else { 338 | return this._startOffset; 339 | } 340 | }, 341 | 342 | /** 343 | * Disconnects and reconnects {@link AudioData} instance to passed 344 | * AudioNode(s) 345 | * 346 | * @param {...Object} nodes - Variable number of AudioNodes to which 347 | * the {@link AudioData} instance shall be reconnected. 348 | */ 349 | reconnect: function(nodes){ 350 | this.disconnect(); 351 | if (Object.prototype.toString.call(nodes) != "[object Array]"){ // == single Node 352 | this.gainNode.connect(nodes); 353 | } 354 | else { // == array of Nodes 355 | for (var i=0; i < nodes.length; i++){ 356 | this.gainNode.connect(nodes[i]); 357 | } 358 | } 359 | }, 360 | 361 | /** 362 | * This method will disconnect the {@link AudioData} instance from 363 | * all connected nodes (afterwards). Should be mostly 364 | * ctx.destination. 365 | */ 366 | disconnect: function(){ 367 | this.gainNode.disconnect(); 368 | }, 369 | 370 | /** 371 | * Method will check whether the passed URL has an extension. 372 | * Additionaly, {@link AudioData#_checkSupport} will be executed to 373 | * identify the possible containers / codecs. 374 | * 375 | * @protected 376 | * @param {string} url - URL 377 | * 378 | * @return {string} src - URL including file type extension which should be 379 | * compatible with browser 380 | */ 381 | _checkExtension: function(url){ 382 | var supports = this._checkSupport(); 383 | 384 | var re = /\.[0-9a-z]{3,4}$/i; // strips the file extension (must be 3 or 4 characters) 385 | var ext = re.exec(url); 386 | if (ext == null){ 387 | if (supports.indexOf(".opus") > -1) { 388 | var src = url + ".opus"; 389 | } 390 | else if (supports.indexOf(".mp4") > -1) { 391 | var src = url + ".mp4"; 392 | } 393 | /* 394 | else if (supports.indexOf(".m4a") > -1) { 395 | var src = url + ".m4a"; 396 | }*/ 397 | else if (supports.indexOf(".ogg") > -1) { 398 | var src = url + ".ogg"; 399 | } 400 | else if (supports.indexOf(".mp3") > -1) { 401 | var src = url + ".mp3"; 402 | } 403 | else if (supports.indexOf(".wav") > -1) { 404 | var src = url + ".wav"; 405 | } 406 | } else { 407 | if (supports.indexOf(ext[0]) > -1){ 408 | var src = url; 409 | } else { 410 | console.error("ERROR: Your browser does not support the needed audio codec (" + ext[0] + ")!"); 411 | var src = ""; 412 | } 413 | } 414 | return src 415 | }, 416 | 417 | /** 418 | * Detects whether the browser can play one of the listed containers 419 | * / codecs 420 | * 421 | * @protected 422 | * @return {string[]} support - An array containing all compatible 423 | * formats 424 | */ 425 | _checkSupport: function (){ 426 | var supports = []; 427 | if (document.createElement('audio').canPlayType("audio/ogg codecs=opus") != ""){ 428 | supports.push(".opus"); 429 | } 430 | if (document.createElement('audio').canPlayType("audio/ogg") != ""){ 431 | supports.push(".ogg"); 432 | } 433 | if (document.createElement('audio').canPlayType("audio/x-wav") != ""){ 434 | supports.push(".wav"); 435 | } 436 | if (document.createElement('audio').canPlayType("audio/mpeg") != ""){ 437 | supports.push(".mp3"); 438 | } 439 | if (document.createElement('audio').canPlayType('audio/mp4') != ""){ 440 | supports.push(".mp4"); 441 | } 442 | if (document.createElement('audio').canPlayType('audio/mp4; codecs="mp4a.40.5"') != ""){ 443 | supports.push(".m4a"); 444 | } 445 | console.debug("Your browser seems to support these containers: " + supports); 446 | return supports; 447 | }, 448 | 449 | /** 450 | * Load passed audio signal 451 | * 452 | * @protected 453 | * @param {string} url - URL 454 | */ 455 | _loadSound: function(url) { 456 | var request = new XMLHttpRequest(); 457 | request.open('GET', url, true); 458 | request.responseType = 'arraybuffer'; 459 | 460 | // Decode asynchronously 461 | var that = this; 462 | request.onload = function() { 463 | that.ctx.decodeAudioData(request.response, function(buffer) { 464 | that._buffer = buffer; 465 | that.canplay = true; 466 | that._rangeEnd = that._buffer.duration; 467 | that.duration = that._buffer.duration; 468 | console.debug("audio loaded & decoded!"); 469 | 470 | /** 471 | * Will be fired if the audio data has been loaded & 472 | * decoded 473 | * @event module:irtPlayer~AudioData#audio_loaded 474 | */ 475 | $(that).triggerHandler("audio_loaded"); 476 | }); 477 | }; 478 | request.send(); 479 | } 480 | } 481 | 482 | 483 | /** 484 | * Represents Controller class which has all the logic to control an 485 | * array of {@link AudioData} instances 486 | * 487 | * @constructor 488 | * 489 | * @param {Object} [ctx] - An AudioContext instance. 490 | * @param {string[]} [sounds] - Array with list of URLs of the audio sources (with or without 491 | * extension). 492 | * @param {boolean} [checkSupportFlag=true] - Enable / disable extension 493 | * support for passed url (see [AudioData._checkExtension]{@link AudioData#_checkExtension}) 494 | * 495 | * @fires module:irtPlayer~IRTPlayer#player_ready 496 | * @fires module:irtPlayer~IRTPlayer#player_ended 497 | */ 498 | var IRTPlayer = function(ctx, sounds, checkSupportFlag){ 499 | if (typeof ctx === 'undefined') { 500 | if (typeof AudioContext !== 'undefined') { 501 | var ctx = new AudioContext(); 502 | } else if (typeof webkitAudioContext !== 'undefined') { 503 | var ctx = new webkitAudioContext(); 504 | } else { 505 | alert("Your browser doesn't support the Web Audio API!"); 506 | } 507 | } 508 | 509 | var checkSupportFlag = typeof checkSupportFlag !== 'undefined' ? checkSupportFlag : true; 510 | this._checkSupport = checkSupportFlag; 511 | this.ctx = ctx; 512 | 513 | /** 514 | * @description Flag if audio signals will be looped 515 | * @var {boolean} */ 516 | this.loopingState = true; 517 | 518 | /** 519 | * @description Array of {@link AudioData} instances 520 | * @var {AudioData[]} */ 521 | this.signals = []; 522 | 523 | /** @var {boolean} */ 524 | this.playing = false; 525 | this.canplay = false; 526 | this.init(sounds); 527 | 528 | /** 529 | * @description Global volume for all {@link AudioData} instances 530 | * @var {float} */ 531 | this.vol = 1.0; 532 | 533 | /** 534 | * @description Has array entry integer of currently active file. 535 | * See {@link IRTPlayer#muteOthers} or {@link IRTPlayer#attenuateOthers} 536 | * @var {integer} 537 | */ 538 | this.activeSignal = null; 539 | //this.muteOthers(0); 540 | this._loaded_counter = 0; 541 | this._ended_counter = 0; 542 | } 543 | 544 | IRTPlayer.prototype = { 545 | 546 | /** 547 | * Adds all audio signals of passed array to the player 548 | * 549 | * @param {string[]} sounds - Array of URLs 550 | */ 551 | init: function(sounds){ 552 | if (typeof sounds != "undefined"){ 553 | for (var i=0; i < sounds.length; i++) { 554 | //this.signals[i] = new AudioData(this.ctx, sounds[i]); // can be also used to reset tracks array 555 | this.addURL(sounds[i]); 556 | } 557 | 558 | // we must bind the event listeners here, because within 559 | // addURL() it would fulfilled every time the event would 560 | // be triggered, since the signals[] array does not yet 561 | // contain all signals during addURL() calls here.. 562 | /* 563 | for (var i=0; i < this.signals.length; i++){ 564 | this._addEventListener(this.signals[i]); 565 | } 566 | */ 567 | } 568 | else { 569 | console.warn('No urls for sounds passed'); 570 | } 571 | }, 572 | 573 | /** 574 | * Will add audio sources manually to the {@link IRTPlayer} instance 575 | * 576 | * @param {string} url - URL of to be added audio source 577 | */ 578 | addURL: function(url){ 579 | var audio = new AudioData(this.ctx, url, this.ctx.destination, this._checkSupport); 580 | this.addAudioData(audio); 581 | 582 | // The event listener must be registered before the event trigger can be 583 | // created! So we call the load() method explicitely afterwards. 584 | audio.load(); 585 | }, 586 | 587 | /** 588 | * Will add {@link AudioData} instances to the {@link IRTPlayer} instance 589 | * 590 | * @param {AudioData} audioData - instance of to be added audio data object 591 | */ 592 | addAudioData: function(audioData){ 593 | this._addEventListener(audioData); 594 | audioData.setLoopState(false); 595 | this.signals.push(audioData); 596 | }, 597 | 598 | _addEventListener: function(audioData){ 599 | // NOTE: This is likely working only due to the delayed loading of 600 | // the audio files. As we all know, the event listener must be already registered 601 | // before the event trigger can be registered as well. So in the worst case, 602 | // the audio files will be loaded and decoded _before_ the listener is 603 | // registered which means that NO event will be triggered and received..! 604 | // TODO: find a good workaround for this issue! 605 | $(audioData).on("audio_loaded", function(){ 606 | this._loaded_counter += 1; 607 | if (this._loaded_counter == this.signals.length){ 608 | console.debug("All buffers are loaded & decoded"); 609 | /** 610 | * Will be fired to the DOM once all audio signals are loaded. 611 | * This event is triggered to the DOM and not to the object instance 612 | * as this would mean that the listener would have to be registered on 613 | * the not yet exisiting object instance... ==> logic proplem. 614 | * TODO: find alternative solution with promises, callback, etc 615 | * @event module:irtPlayer~IRTPlayer#player_ready 616 | */ 617 | $(this).triggerHandler("player_ready"); 618 | this.canplay = true; 619 | this.duration = this.signals[0].duration; 620 | } 621 | }.bind(this)); 622 | 623 | $(audioData).on("audio_ended", function(){ 624 | this._ended_counter += 1; 625 | if (this._ended_counter == this.signals.length){ 626 | this.playing = false; 627 | console.debug("All buffers ended"); 628 | /** 629 | * Will be fired to the DOM once all audio signals are loaded. 630 | * This event is triggered to the DOM and not to the object instance 631 | * as this would mean that the listener would have to be registered on 632 | * the not yet exisiting object instance... ==> logic proplem. 633 | * TODO: find alternative solution with promises, callback, etc 634 | * @event module:irtPlayer~IRTPlayer#player_ended 635 | */ 636 | $(this).triggerHandler("player_ended"); 637 | } 638 | }.bind(this)); 639 | }, 640 | 641 | /** 642 | * Toggles play / pause of playback 643 | */ 644 | togglePlay: function(){ 645 | if (this.playing == false){ 646 | this.play(); 647 | } 648 | else { 649 | this.pause(); 650 | } 651 | }, 652 | 653 | /** 654 | * Starts playback of all audio sources in {@link IRTPlayer#signals} 655 | */ 656 | play: function(){ 657 | this._do('play'); 658 | this.playing = true; 659 | this._do('setLoopState', this.loopingState); 660 | this._ended_counter = 0; 661 | }, 662 | 663 | /** 664 | * Pauses playback of all audio sources in {@link IRTPlayer#signals} 665 | */ 666 | pause: function(){ 667 | this._do('pause'); 668 | this.playing = false; 669 | }, 670 | 671 | /** 672 | * Stops playback of all audio sources in {@link IRTPlayer#signals} 673 | */ 674 | stop: function(){ 675 | this._do('stop'); 676 | this.playing = false; 677 | this._do("setLoopState", false); 678 | }, 679 | 680 | /** 681 | * Will mute all audio sources of {@link IRTPlayer#signals} but the 682 | * one with the passed index 683 | * 684 | * @param {integer} id - Array index number of active audio source 685 | */ 686 | muteOthers: function(id){ 687 | id = parseInt(id); 688 | if ((id < this.signals.length) && (id >= 0)){ 689 | this._do('mute'); 690 | this.signals[id].unmute(); 691 | this.activeSignal = id; 692 | } 693 | else{ 694 | console.error("Passed array index invalid!") 695 | } 696 | }, 697 | 698 | /** 699 | * Will unmute all audio sources in {@link IRTPlayer#signals} 700 | */ 701 | unmuteAll: function(){ 702 | this._do('unmute'); 703 | this.activeSignal = null; 704 | }, 705 | 706 | /** 707 | * Will attenuate all audio sources of {@link IRTPlayer#signals} but the 708 | * one with the passed index. The active one will have gain value of 709 | * {@link IRTPlayer#vol} 710 | * 711 | * @param {integer} id - Array index number of active audio source 712 | * @param {float} attenuation - Gain value for other (attenuated) 713 | * audio sources 714 | */ 715 | attenuateOthers: function(id, attenuation){ 716 | id = parseInt(id); 717 | if ((id < this.signals.length) && (id >= 0)){ 718 | this._do('setGain', attenuation); 719 | this.signals[id].setGain(this.vol); 720 | this.activeSignal = id; 721 | } 722 | else{ 723 | console.error("Passed array index invalid!") 724 | } 725 | }, 726 | 727 | /** 728 | * Disables / enables looping of the audio sources 729 | */ 730 | toggleLoop: function() { 731 | if (this.loopingState == false){ 732 | this.loopingState = true; 733 | } 734 | else { 735 | this.loopingState = false; 736 | } 737 | this._do('toggleLoop'); 738 | }, 739 | 740 | /** 741 | * Sets start position for playback 742 | * 743 | * @param {float} pos - Start playback always at passed 744 | * position for all audio sources in {@link IRTPlayer#signals} 745 | */ 746 | setRangeStart: function(pos){ 747 | console.info("Range start: " + pos); 748 | this._do('setRangeStart', pos); 749 | }, 750 | 751 | /** 752 | * Sets end position for playback 753 | * 754 | * @param {float} pos - End playback always at passed 755 | * position for all audio sources in {@link IRTPlayer#signals} 756 | */ 757 | setRangeEnd: function(pos){ 758 | console.info("Range end: " + pos); 759 | this._do('setRangeEnd', pos); 760 | }, 761 | 762 | /** 763 | * Jump to passed position during playback 764 | * 765 | * @param {float} time - Must be between 0 and {@link 766 | * AudioData#_rangeEnd} 767 | */ 768 | setTime: function(time){ 769 | this._do('setTime', time); 770 | }, 771 | 772 | /** 773 | * Returns current position of playback 774 | * @return {number} pos - Current playback position 775 | */ 776 | getTime: function(){ 777 | return this.signals[0].getTime(); 778 | }, 779 | 780 | /** 781 | * Helper function to apply AudioData methods for all instances in 782 | * {@link IRTPlayer#signals} array 783 | * @param {string} func - Name of the method to be executed 784 | * @param {...args} args - variable number of additional arguments that 785 | * should be passed to the method 786 | * @protected 787 | */ 788 | _do: function(func){ 789 | if (arguments.length == 2){ 790 | var args = arguments[1]; // prevents that a single argument will be passed as array with one entry 791 | } else { 792 | var args = Array.prototype.splice.call(arguments, 1); 793 | } 794 | for (var i=0; i < this.signals.length; i++){ 795 | this.signals[i][func](args); 796 | } 797 | } 798 | } 799 | 800 | 801 | exports.AudioData = AudioData; 802 | exports.IRTPlayer = IRTPlayer; 803 | -------------------------------------------------------------------------------- /src/media_controller.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | /** 3 | * @file media_controller.js 4 | * @author Michael Weitnauer: {@link weitnauer@irt.de} 5 | */ 6 | 7 | /** 8 | * @module bogJS 9 | * 10 | */ 11 | 12 | var GainController = require('./gain_controller'); 13 | 14 | /** 15 | * Represents MediaElementController class which has all the logic to control a HTML5 media element 16 | * Every track / channel of the media element can be controlled separately. 17 | * 18 | * @constructor 19 | * 20 | * @param {Object} ctx - An AudioContext instance. 21 | * @param {string} mediaElement - HTML5 media element 22 | * @param {Number} tracks - Number of media element channels 23 | * @param {Object} [targetNodeList=ctx.destination] - The audio node to which the MediaElementController 24 | * @fires module:bogJS~MediaElementController#audio_init 25 | * @fires module:bogJS~MediaElementController#audio_loaded 26 | * @fires module:bogJS~MediaElementController#audio_ended 27 | */ 28 | var MediaElementController = function(ctx, mediaElement, tracks, targetNodeList) { 29 | /** @protected 30 | * @var {boolean} */ 31 | this.canplay = false; 32 | 33 | /** @var {Object.} */ 34 | this.ctx = ctx; 35 | 36 | this._mediaElement = mediaElement; 37 | this._mediaSourceNode = this.ctx.createMediaElementSource(this._mediaElement); 38 | this._tracks = tracks; 39 | this._splitter = this.ctx.createChannelSplitter(this._tracks); 40 | this._mediaSourceNode.connect(this._splitter); 41 | 42 | this.gainController = []; 43 | if (typeof targetNodeList === 'undefined') { 44 | var targetNodeList = []; 45 | for (var i = 0; i < this._tracks; i++){ 46 | targetNodeList.push(this.ctx.destination); 47 | } 48 | } 49 | for (var i = 0; i < this._tracks; i++){ 50 | this.gainController[i] = new GainController(this.ctx, targetNodeList[i]); 51 | 52 | // TODO: Workaround for wrong channel order of decoded bitstream 53 | this._splitter.connect(this.gainController[i].gainNode, i); 54 | } 55 | 56 | this._mediaElement.onended = function(){ 57 | console.debug("Audio buffer has ended!"); 58 | this._playing = false; 59 | 60 | /** 61 | * Will be fired once the playback has ended 62 | * @event module:bogJS~MediaElementController#audio_ended 63 | */ 64 | $(this).triggerHandler("audio_ended"); 65 | }.bind(this); 66 | 67 | this._mediaElement.onstalled = function(){ 68 | console.info("Pausing playback - need to buffer more"); 69 | this.ctx.suspend(); 70 | }.bind(this); 71 | 72 | this._mediaElement.onplaying = function(){ 73 | console.info("Resuming playback of media element"); 74 | if (this.ctx.state === "suspended"){ 75 | this.ctx.resume(); 76 | } 77 | }.bind(this); 78 | 79 | this._mediaElement.oncanplaythrough = function(){ 80 | this.canplay = true; 81 | console.info("Playback of media element can start"); 82 | 83 | /** 84 | * Will be fired if media element playback can start 85 | * @event module:bogJS~MediaElementController#audio_loaded 86 | */ 87 | $(this).triggerHandler('audio_loaded'); 88 | if (this.ctx.state === "suspended"){ 89 | this.ctx.resume(); 90 | } 91 | }.bind(this); 92 | 93 | this._mediaElement.load(); 94 | this._playing = false; 95 | this._looping = false; 96 | } 97 | 98 | MediaElementController.prototype = { 99 | /** 100 | * Start playback of audio signal 101 | * 102 | * @param {number} [pos] - Position from which the playback shall start 103 | * (optional) 104 | */ 105 | play: function(pos){ 106 | if (typeof pos != 'number'){ // detection with _.isNumber() could be more robust 107 | this._mediaElement.play(); 108 | } else { 109 | console.debug("Starting playback at " + pos); 110 | this.setTime(pos); 111 | this._mediaElement.play() 112 | } 113 | this._playing = true; 114 | }, 115 | 116 | /** 117 | * Pause playback. 118 | * 119 | */ 120 | pause: function(){ 121 | this._mediaElement.pause(); 122 | this._playing = false; 123 | }, 124 | 125 | /** 126 | * Stops playback. 127 | */ 128 | stop: function(){ 129 | this._mediaElement.pause(); 130 | this._playing = false; 131 | this._mediaElement.currentTime = 0; 132 | }, 133 | 134 | /** 135 | * Sets gain of {@link MediaElementController} instance 136 | * 137 | * @param {float} gain - Value between 0.0 and 1.0 138 | */ 139 | setVolume: function(vol){ 140 | this._mediaElement.volume = vol; 141 | }, 142 | 143 | /** 144 | * Returns current gain value of {@link MediaElementController} instance 145 | * 146 | * @return {float} value - Float gain value 147 | */ 148 | getVolume: function(){ 149 | return this._mediaElement.volume; 150 | }, 151 | 152 | /** 153 | * Disables / enables the loop of the {@link MediaElementController} instance 154 | */ 155 | toggleLoop: function() { 156 | if (this._looping == false){ 157 | this._looping = true; 158 | } else { 159 | this._looping = false; 160 | } 161 | this._mediaElement.loop = this._looping; 162 | }, 163 | 164 | /** 165 | * Disables / enables the loop of the {@link MediaElementController} instance 166 | */ 167 | setLoopState: function(bool) { 168 | this._looping = bool; 169 | this._mediaElement.loop = this._looping; 170 | }, 171 | 172 | /** 173 | * Mutes {@link MediaElementController} instance 174 | */ 175 | mute: function(){ 176 | this._mediaElement.muted = true; 177 | }, 178 | 179 | /** 180 | * Unmutes {@link MediaElementController} instance 181 | */ 182 | unmute: function(){ 183 | this._mediaElement.muted = false; 184 | }, 185 | 186 | /** 187 | * Jump to passed position during playback 188 | * 189 | * @param {float} pos - Must be >= 0 190 | */ 191 | setTime: function(pos){ 192 | if (pos >= 0){ 193 | this._mediaElement.currentTime = pos; 194 | } 195 | }, 196 | 197 | /** 198 | * Returns current playback position 199 | * 200 | * @return {number} value - Current playback position 201 | */ 202 | getTime: function(){ 203 | return this._mediaElement.currentTime; 204 | } 205 | } 206 | 207 | module.exports = MediaElementController; 208 | -------------------------------------------------------------------------------- /src/object.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | /** 3 | * @file object.js 4 | * @author Michael Weitnauer [weitnauer@irt.de] 5 | */ 6 | 7 | /** 8 | * @module bogJS 9 | */ 10 | 11 | var GainController = require('./gain_controller'); 12 | 13 | /** 14 | * Represents ObjectController class which has all the logic to control an 15 | * audio object 16 | * 17 | * @constructor 18 | * 19 | * @param {Object} ctx - An AudioContext instance. 20 | * @param {AudioData} sourceNode - Instance of an {@link 21 | * module:irtPlayer~AudioData|AudioData} object. 22 | * @param {AudioData} [targetNode=ctx.destination] - Instance of an 23 | * Web Audio API node to which the output of the ObjectController 24 | * shall be connected to. 25 | */ 26 | 27 | var ObjectController = function(ctx, sourceNode, targetNode=ctx.destination) { 28 | /** 29 | * Instance of Web Audio Panner node 30 | * @var {Object.} 31 | */ 32 | this.panner = ctx.createPanner(); 33 | 34 | // Experimental highpass to avoid sizzling noinse while chaning view / angle 35 | //this.highpass = ctx.createBiquadFilter(); 36 | //this.highpass.type = "highpass"; 37 | //this.setHighpassFreq(80); 38 | //this.highpass.connect(this.panner); 39 | 40 | /** 41 | * Has the current panning mode of the object 42 | * @readonly 43 | */ 44 | this.panningType = "equalpower"; 45 | this.panner.maxDistance = 10; 46 | 47 | this.setPanningType(this.panningType); 48 | this.position = [0, 0, 0]; // FIXME: make private and use set and get methods 49 | this.gain = 1; // valid values between 0 and 1 // FIXME: make private and use set and get methods 50 | 51 | this._state = false; 52 | this.stateNode = new GainController(ctx, this.panner); 53 | this.interactiveGain = new GainController(ctx, this.stateNode.gainNode); 54 | 55 | this.setAudio(sourceNode); 56 | this.panner.connect(targetNode); 57 | }; 58 | 59 | ObjectController.prototype = { 60 | 61 | /** 62 | * Change position of panner object within 3D space 63 | * 64 | * @param {Float[]} xyz - An array with three entries: [x, y, z] 65 | 66 | * @see Interpolation as per AudioParam Interface not possible with 67 | * current WAA version. The PannerNode will be deprecated in V1 68 | * and a new SpatializerNode will be introduced that should 69 | * support interpolation _and_ loading own HRTF databases!! 70 | * {@link https://github.com/WebAudio/web-audio-api/issues/372| GitHub issue 372} 71 | */ 72 | setPosition: function(xyz){ 73 | var my_xyz = [parseFloat(xyz[0]), parseFloat(xyz[1]), parseFloat(xyz[2])]; 74 | this.panner.setPosition(xyz[0], xyz[1], xyz[2]); 75 | console.debug("New Position: " + my_xyz); 76 | this.position = xyz; 77 | }, 78 | 79 | /** 80 | * Get current Position of object 81 | * @return {Float[]} position - Array with current [x, y, z] values 82 | */ 83 | getPosition: function(){ 84 | return this.position; 85 | }, 86 | 87 | /** 88 | * Enabling / disabling the object 89 | * 90 | * @param {Boolean} state - Enables / disables the panner object 91 | * instance 92 | */ 93 | setStatus: function(state){ 94 | if ((state === true) || (state == 1)){ 95 | this.stateNode.unmute(); 96 | this._state = true; 97 | } 98 | else if ((state === false) || (state == 0)){ 99 | this.stateNode.mute(); 100 | this._state = false; 101 | } 102 | console.info("Setting state to " + this._state); 103 | }, 104 | 105 | /** 106 | * Sets gain value of {@link 107 | * module:bogJS~GainController#gainNode|GainController.gainNode} 108 | * Separate GainNode to be used for interactive Gain control, aka 109 | * cross-fading between one group and another. 110 | * @param {Float} gain - Must be between 0.0 and 1.0 111 | */ 112 | setInteractiveGain: function(gain){ 113 | this.interactiveGain.setGain(gain); 114 | this._interactiveGain = gain; 115 | }, 116 | 117 | /** 118 | * Returns current object state 119 | * @return {Boolean} status 120 | */ 121 | getStatus: function(){ 122 | return this._state; 123 | }, 124 | 125 | /** 126 | * Sets gain value of {@link 127 | * module:irtPlayer~AudioData#gainNode|AudioData.gainNode} 128 | * 129 | * @param {Float} gain - Must be between 0.0 and 1.0 130 | * @param {Float} [time=Now] - At which time shall the gain value be applied 131 | * @param {Boolean} [interpolation=false] - Set to true if gain 132 | * value shall be linear faded to passed gain value from passed time on. If 133 | * false, the gain value will be applied immediately. 134 | */ 135 | setGain: function(gain, time="now", interpolation=false){ 136 | if (time === "now") { 137 | this.audio.setGain(gain); 138 | this.gain = gain; 139 | } 140 | else if ((time !== "now") && (interpolation === false)) { 141 | this.audio.gainNode.gain.setValueAtTime(gain, time); 142 | } 143 | else if ((time !== "now") && (interpolation !== false)){ 144 | this.audio.gainNode.gain.linearRampToValueAtTime(gain, time); 145 | } 146 | }, 147 | 148 | /** 149 | * Get current gain value of {@link 150 | * module:irtPlayer~AudioData#gainNode|AudioData.gainNode} 151 | * 152 | * @return {Float} gain 153 | */ 154 | getGain: function(){ 155 | return this.audio.getGain(); // or do we trust in this.gain ?? 156 | }, 157 | 158 | /** 159 | * Set panning type of Panner object instance. 160 | * Currently, "equalpower" only supports Stereo (2ch) panning. 161 | * 162 | * @param {("HRTF"|"equalpower")} panningType - Choose "HRTF" for binaural 163 | * rendering or "equalpower" for Stereo rendering. 164 | */ 165 | setPanningType: function(panningType){ 166 | if ((panningType === "HRTF") || (panningType === "equalpower")){ 167 | this.panner.panningModel = panningType; 168 | this.panningType = this.panner.panningModel; 169 | } 170 | else { 171 | console.error("Only >>HRTF<< or >>equalpower<< are valid types"); 172 | } 173 | }, 174 | 175 | /** 176 | * Get panning type 177 | * @return {("HRTF"|"equalpower")} panningType - Either "HRTF" or "equalpower" 178 | */ 179 | getPanningType: function(){ 180 | return this.panner.panningModel; 181 | }, 182 | 183 | /** 184 | * Sets the double value describing how quickly the volume is reduced 185 | * as the source moves away from the listener. The initial default value 186 | * is 1. This value is used by all distance models. 187 | * 188 | * @param {Float} factor 189 | */ 190 | setRollOffFactor: function(factor){ 191 | this.panner.rolloffFactor = factor; 192 | }, 193 | 194 | /** 195 | * Sets the value determining which algorithm to use to reduce the 196 | * volume of the audio source as it moves away from the listener. The 197 | * initial default value is "inverse" which should be equivalent to 1/r. 198 | * 199 | * @param {("inverse"|"exponential"|"linear")} model - "inverse" is the default setting 200 | */ 201 | setDistanceModel: function(model){ 202 | this.panner.distanceModel = model; 203 | }, 204 | 205 | /** 206 | * Sets the value representing the reference distance for reducing volume 207 | * as the audio source moves further from the listener. The initial 208 | * default value is 1. This value is used by all distance models. 209 | * 210 | * @param {float} refDistance 211 | */ 212 | setRefDistance: function(refDistance){ 213 | this.panner.refDistance = refDistance; 214 | }, 215 | 216 | /** 217 | * Sets the value representing the maximum distance between the audio 218 | * source and the listener, after which the volume is not reduced any 219 | * further. The initial default value is 10000. This value is used 220 | * only by the linear distance model. 221 | * 222 | * @param {float} maxDistance 223 | */ 224 | setMaxDistance: function(maxDistance){ 225 | this.panner.maxDistance = maxDistance; 226 | }, 227 | 228 | /** 229 | * Connects the input of the ObjectController instance 230 | * with the output of the passed audioNode. 231 | * 232 | * @param {AudioData} audioNode - Instance of an {@link 233 | * module:irtPlayer~AudioData|AudioData} or GainController object. 234 | */ 235 | setAudio: function(audioNode){ 236 | // call disconnect only if this.audio exists 237 | // it is absolutely essential to disconnect the old audio instance 238 | // before the new one can be assigned! 239 | /* FIXME: clarify the expected behaviour of a setAudio() method! 240 | if (this.audio){ 241 | this.audio.disconnect(this.panner); 242 | } 243 | */ 244 | this.audio = audioNode; 245 | // just to make sure we assigned a valid audioNode.. 246 | if (this.audio){ 247 | // FIXME: AudioData() class should also have a connect method. 248 | // Better would be to use derived class mechanisms. 249 | if(this.audio.connect) { 250 | this.audio.connect(this.interactiveGain.gainNode); 251 | } 252 | else { 253 | this.audio.reconnect(this.interactiveGain.gainNode); 254 | } 255 | } 256 | }, 257 | 258 | setHighpassFreq: function(freq){ 259 | this.highpass.frequency.value = freq; 260 | } 261 | }; 262 | 263 | module.exports = ObjectController; 264 | -------------------------------------------------------------------------------- /src/object_manager.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | /** 3 | * @file object_manager.js 4 | * @author Michael Weitnauer [weitnauer@irt.de] 5 | */ 6 | 7 | /** 8 | * @module bogJS 9 | */ 10 | 11 | /** 12 | * @typedef keyframes 13 | * @type {object} 14 | * @example 15 | * keyframes = {0.0: [{obj: "Cello", cmd: "position", params: [3.2, 4, 0]}, 16 | * {obj: "Cembalo", cmd: "gain", params: 0.5}], 17 | * 0.4: [{obj: "Cembalo", cmd: "is_present", params: 0}, 18 | * {obj: "Cello", cmd: "gain", params: 1.0}], 19 | * 235: [{obj: "Viola", cmd: "is_present", params: 1}, 20 | * {obj: "Viola", cmd: "position", params: [0.5, 3.2, 0.5]}]}; 21 | */ 22 | 23 | /** 24 | * @typedef audioURLs 25 | * @type {object} 26 | * @example 27 | * audioURLs = {Cello: "http://sounds.myserver.com/Cello.ogg", 28 | * Cembalo: "http://sounds.myserver.com/Cembalo.wav", 29 | * Viola: "../../Viola.m4a"} 30 | */ 31 | 32 | /** 33 | * @typedef sceneInfo 34 | * @type {object} 35 | * @example 36 | * sceneInfo = {name: "My fancy scene", 37 | * listener_position: [0, 0, 0], 38 | * listener_orientation: [0, 1, 0], 39 | * object_count: 3, 40 | * room_dimensions: [10, 10, 3]} 41 | */ 42 | 43 | /** 44 | * @typedef singleObjects 45 | * @type {object} 46 | * @example 47 | * singleObjects = {"70.754":["Birds1_L","Birds1_R"], 48 | * "72.0":["Birds2_L","Birds2_R"], 49 | * "79.29":["Birds3"], 50 | * "90.65":["Crows"], 51 | * "102.55":["Vulcano_L","Vulcano_R"], 52 | * "117.55":["Stones_L","Stones_R"]} 53 | */ 54 | 55 | /** 56 | * @typedef groupObjects 57 | * @type {object} 58 | * @example 59 | * groupObjects = {"78.2":["Birds1_L","Birds1_R"], 60 | * "90.65":["Birds"], 61 | * "117.55":["Stones_L","Stones_R"]} 62 | */ 63 | 64 | /** 65 | * @typedef audiobeds 66 | * @type {object} 67 | * @example 68 | * audiobeds = {Bed0: "bed_0", Bed1: "bed_1", Bed2: "bed_2", Bed3: "bed_3", Bed4: "bed_4"} 69 | */ 70 | 71 | window._ = require('underscore'); 72 | var WAAClock = require('waaclock'); 73 | var ChannelOrderTest = require('./channelorder_test'); 74 | var AudioData = require('./html5_player/core').AudioData; 75 | var IRTPlayer = require('./html5_player/core').IRTPlayer; 76 | var ObjectController = require('./object'); 77 | var GainController = require('./gain_controller'); 78 | var MediaElementController = require('./media_controller'); 79 | var SceneReader = require('./scene_reader'); 80 | 81 | /** 82 | * Represents ObjectManager class which has all the logic to control 83 | * several {@link module:bogJS~ObjectController|ObjectController} instances along with metadata 84 | * 85 | * @constructor 86 | * 87 | * @param {string} url - URL of the metadata source. 88 | * @param {Object} [ctx] - An AudioContext instance. 89 | * @param {Object} [reader] - A reader instance that has a load() method 90 | * and will fire a event called "scene_loaded". The event must also pass 91 | * {@link module:bogJS~keyframes|keyframes}, {@link module:bogJS~audioURLs|audioURLs} 92 | * and {@link module:bogJS~sceneInfo|sceneInfo}. See {@link 93 | * module:bogJS~SceneReader#load|SceneReader.load()} 94 | * @param {Object} [mediaElement] - A HMTL5 media element instance to be used as 95 | * audio bed. If passed, any potentially other given audio bed from the scene 96 | * file will be ignored. 97 | * @param {Number} [audiobed_tracks] - If mediaElement is passed, the expected 98 | * channel number must be passed as well. 99 | * @param {String} [channelorder_root] - Path to encoded channel order detection 100 | * test files. See also [ChannelOrderTest]{@link module:bogJS~ChannelOrderTest} 101 | * and the README.md file. 102 | * @fires module:bogJS~ObjectManager#om_newGain 103 | * @fires module:bogJS~ObjectManager#om_newPosition 104 | * @fires module:bogJS~ObjectManager#om_newTrackMapping 105 | * @fires module:bogJS~ObjectManager#om_isActive 106 | * 107 | */ 108 | var ObjectManager = function(url, ctx, reader, mediaElement, audiobed_tracks, channelorder_root){ 109 | if (typeof ctx === 'undefined') { 110 | if (typeof AudioContext !== 'undefined') { 111 | var ctx = new AudioContext(); 112 | } else if (typeof webkitAudioContext !== 'undefined') { 113 | var ctx = new webkitAudioContext(); 114 | } else { 115 | alert("Your browser doesn't support the Web Audio API!"); 116 | } 117 | } 118 | /** 119 | * Instance of Web Audio AudioContext 120 | * @var {Object.} 121 | */ 122 | this.ctx = ctx; 123 | this.masterGain = new GainController(this.ctx, ctx.destination); 124 | /** 125 | * Instance of {@link SceneReader} 126 | * @var {(CustomReaderInstance|Object.)} 127 | */ 128 | this.reader = reader || new SceneReader(); 129 | 130 | this._mediaElement = mediaElement; 131 | this._mediaElementTracks = audiobed_tracks; 132 | this._channorder_root = channelorder_root; 133 | 134 | /** 135 | * Instance of {@link module:irtPlayer~IRTPlayer|IRTPlayer} 136 | * @var {Object.} 137 | */ 138 | this._clock = new WAAClock(this.ctx); 139 | this._evts = {}; 140 | this._timer_evt = false; 141 | this._audioURLs = {}; 142 | this._currentKeyframeIndex = 0; 143 | this._kfMapping = {}; 144 | this._last_kfMapping = {}; 145 | 146 | this._audiobedTracks = {}; 147 | this._groupObjURLs = {}; 148 | this._singleObjURLs = {}; 149 | this._audiobed = false; 150 | this._groupObjPlayers = {}; 151 | this._singleObjAudios = {}; 152 | this._kf_canplay = {}; 153 | 154 | /** 155 | * Array of all {@link module:bogJS~ObjectController|ObjectController} instances that are controlled 156 | * by the {@link module:bogJS~ObjectManager|ObjectManager} 157 | * @var {module:bogJS~ObjectController[]} 158 | */ 159 | this.objects = {}; 160 | this._audioInstances = {}; 161 | this._panningType = "equalpower"; 162 | 163 | /** 164 | * If set to true, the ObjectManager will ignore keyframe updates! 165 | * @var {boolean} 166 | * @default false 167 | */ 168 | this.interactive = false; 169 | this.playing = false; 170 | 171 | this._listenerOrientation = [0, 0, -1]; 172 | this.setListenerOrientation(0, 0, -1); 173 | 174 | $(this.reader).on('scene_loaded', function(e, keyframes, audioURLs, sceneInfo, groupObjects, singleObjects, audiobeds, interactiveInfo){ 175 | console.debug('Scene data loaded!'); 176 | 177 | /** 178 | * 'Dictionary' containing keyframes + commands triplets per keyframe. 179 | * @abstract 180 | * @var {module:bogJS~keyframes} 181 | */ 182 | this._keyframes = keyframes; 183 | 184 | /** 185 | * 'Dictionary' containing mapping for objects and URLs. 186 | * @abstract 187 | * @var {module:bogJS~audioURLs} 188 | */ 189 | this._audioURLs = audioURLs; 190 | 191 | /** 192 | * 'Dictionary' containing additional scene info 193 | * @abstract 194 | * @var {module:bogJS~sceneInfo} 195 | */ 196 | this._sceneInfo = sceneInfo; 197 | /** 198 | * 'Dictionary' containing interactive info 199 | * @abstract 200 | * @var {module:bogJS~interactiveInfo} 201 | */ 202 | this.interactiveInfo = interactiveInfo; 203 | this.object_count = sceneInfo.object_count || 0; 204 | this.roomDimensions = sceneInfo.room_dimensions || [10, 10, 3]; 205 | this._listenerPosition = sceneInfo.listener_position || [0, 0, 0]; 206 | 207 | /** 208 | * 'Dictionary' containing mapping for objects and audiobed tracks 209 | * @abstract 210 | * @var {module:bogJS~audiobeds} 211 | */ 212 | this._audiobedTracks = audiobeds; 213 | 214 | /** 215 | * 'Dictionary' containing info to identify grouped objects 216 | * @abstract 217 | * @var {module:bogJS~groupObjects} 218 | */ 219 | this._groupObjURLs = groupObjects; 220 | 221 | /** 222 | * 'Dictionary' containing info to identify single objects 223 | * @abstract 224 | * @var {module:bogJS~singleObjects} 225 | */ 226 | this._singleObjURLs = singleObjects; 227 | 228 | this.init(); 229 | }.bind(this)); 230 | this.reader.load(url); 231 | }; 232 | 233 | ObjectManager.prototype = { 234 | 235 | /** 236 | * Creates [AudioData]{@link module:irtPlayer~AudioData} and 237 | * [ObjectController]{@link module:bogJS~ObjectController} instances and 238 | * adds the AudioData instances to the {@link module:bogJS~ObjectManager#player} 239 | */ 240 | init: function(){ 241 | if (typeof this._mediaElement !== 'undefined'){ 242 | this._audiobed = new MediaElementController(this.ctx, this._mediaElement, this._mediaElementTracks); 243 | } else if (this._sceneInfo.audiobed_url){ 244 | var a = document.createElement("audio"); 245 | var src = this._sceneInfo.audiobed_url; 246 | if (/\.[0-9a-z]{3,4}$/i.exec(src) === null){ // if no file extension is stated 247 | if (a.canPlayType('audio/ogg codecs=opus')) { 248 | a.type= 'audio/ogg codecs=opus'; 249 | src = src + '.opus'; 250 | } else { 251 | a.type = 'audio/mp4'; 252 | src = src + '.mp4'; 253 | } 254 | } 255 | a.src = src; 256 | this._mediaElementTracks = parseInt(this._sceneInfo.audiobed_tracks); 257 | this._audiobed = new MediaElementController(this.ctx, a, this._mediaElementTracks); 258 | } 259 | if (this._audiobed !== false){ 260 | // If there is an audiobed, we can trigger the om_ready event even 261 | // though other keyframe assets are not yet ready. We need to trigger 262 | // the event here in case NO other assets are used. 263 | // This is for sure not really a sophisticated way to solve this but it 264 | // should work. In the worst case, the playback will pause again if 265 | // the assets are not yet loaded and decoded. 266 | $(this._audiobed).on('audio_loaded', function(){ 267 | console.debug("Audiobed loaded, detect channel order.."); 268 | var url = ""; 269 | if (this._audiobed._mediaElement.src !== ""){ 270 | url = this._audiobed._mediaElement.src; 271 | } else if (this._audiobed._mediaElement.currentSrc !== "") { 272 | url = this._audiobed._mediaElement.currentSrc; 273 | } else { 274 | console.error("The src of the audiobed couldn't be detected!"); 275 | } 276 | var re = /\.[0-9a-z]{3,4}$/i; // strips the file extension (must be 3 or 4 characters) 277 | var container = re.exec(url)[0]; 278 | container = container.split('.').join(""); // removes dot from the string 279 | this._chOrderTest = new ChannelOrderTest(container, 280 | this._mediaElementTracks, 281 | this.ctx, 282 | this._channorder_root); 283 | $(document).triggerHandler('om_ready'); 284 | console.debug('Audiobed ready for playback'); 285 | //var chOrder = this._chOrderTest.testChs(); 286 | }.bind(this)); 287 | 288 | $(this._audiobed).on('audio_ended', function(){ 289 | $(document).triggerHandler('om_ended'); 290 | om.stop(); 291 | }.bind(this)); 292 | 293 | $(document).on('order_ready', function(e, order){ 294 | console.debug('Got channel order: ' + order); 295 | this._chOrder = order; 296 | // firstly, disconnect any connections to other nodes to avoid 297 | // confusions and strange behaviours.. 298 | for (var i = 0; i < order.length; i++){ 299 | this.objects["Bed"+order[i]].audio.disconnect(); 300 | } 301 | // now assign correct gainController to corresponding 302 | // pannerNode 303 | for (var i = 0; i < order.length; i++){ 304 | console.debug("Reconnecting GainController " + i + " with Bed " + order[i]); 305 | this.objects["Bed"+order[i]].setAudio(this._audiobed.gainController[i]); 306 | } 307 | }.bind(this)); 308 | 309 | } 310 | 311 | for (var obj in this._audiobedTracks){ 312 | var trackNr = parseInt(this._audiobedTracks[obj].split("_")[1]); 313 | this.objects[obj] = new ObjectController(this.ctx, 314 | this._audiobed.gainController[trackNr], 315 | this.masterGain.gainNode); 316 | this.objects[obj].audio._id = obj; 317 | this.objects[obj].panner._id = obj; 318 | } 319 | 320 | for (var kf in this._groupObjURLs){ 321 | this._groupObjPlayers[kf] = {}; 322 | this._kf_canplay[kf] = {}; 323 | for (var group in this._groupObjURLs[kf]){ 324 | this._kf_canplay[kf][group] = false; 325 | var player = new IRTPlayer(this.ctx); 326 | $(player).on('player_ready', this._loadedStateDelegate(kf, group)); 327 | for (var idx in this._groupObjURLs[kf][group]){ 328 | var obj = this._groupObjURLs[kf][group][idx]; 329 | var url = this._audioURLs[obj]; 330 | var audioInstance = new AudioData(this.ctx, url); 331 | audioInstance.load(); 332 | audioInstance.setLoopState(false); 333 | this.objects[obj] = new ObjectController(this.ctx, 334 | audioInstance, 335 | this.masterGain.gainNode); 336 | player.addAudioData(audioInstance); 337 | this._groupObjPlayers[kf][group] = player; 338 | } 339 | } 340 | } 341 | 342 | for (var kf in this._singleObjURLs){ 343 | this._singleObjAudios[kf] = {}; 344 | if (!this._kf_canplay[kf]){ 345 | this._kf_canplay[kf] = {}; 346 | } 347 | for (var idx in this._singleObjURLs[kf]){ 348 | var obj = this._singleObjURLs[kf][idx]; 349 | var url = this._audioURLs[obj]; 350 | this._kf_canplay[kf][obj] = false; 351 | var audioInstance = new AudioData(this.ctx, url); 352 | $(audioInstance).on("audio_loaded", this._loadedStateDelegate(kf, obj)); 353 | audioInstance.load(); 354 | audioInstance.setLoopState(false); 355 | this.objects[obj] = new ObjectController(this.ctx, 356 | audioInstance, 357 | this.masterGain.gainNode); 358 | this._singleObjAudios[kf][obj] = audioInstance; 359 | } 360 | } 361 | this.setPanningType(this._panningType); 362 | $(document).triggerHandler('om_initialized'); 363 | console.debug('Scene sucessfully initialized!'); 364 | if (this.interactiveInfo.switchGroups){ 365 | for (var g of Object.keys(this.interactiveInfo.switchGroups)){ 366 | this._initSwitchGroup(g); 367 | } 368 | } 369 | //this.start(); 370 | }, 371 | 372 | /** 373 | * Starts playback and rendering of audio scene 374 | */ 375 | start: function(){ 376 | if ((this._checkReadyStart() === true) && (this.playing === false)) { 377 | this._clock.start(); 378 | this._startTime = this.ctx.currentTime; 379 | this._processCurrentKeyframes(); 380 | if (this._audiobed !== false){ 381 | this._audiobed.play(); 382 | } 383 | var that = this; 384 | if (!this._timer_evt){ 385 | this._timer_evt = this._clock.setTimeout(function(){ 386 | console.debug(that.ctx.currentTime); 387 | }, 1).repeat(1); 388 | } 389 | this.playing = true; 390 | return true; 391 | } else { 392 | console.info("Audio signals not yet ready for playing."); 393 | return false; 394 | } 395 | }, 396 | 397 | /** 398 | * Pauses playback 399 | */ 400 | pause: function(){ 401 | this.ctx.suspend(); 402 | if (this._audiobed !== false){ 403 | this._audiobed.pause(); 404 | } 405 | this.playing = false; 406 | }, 407 | 408 | /** 409 | * Resumes playback of all objects if paused. 410 | * 411 | */ 412 | resume: function(){ 413 | this.ctx.resume(); 414 | if (this._audiobed !== false){ 415 | this._audiobed.play(); 416 | } 417 | this.playing = true; 418 | }, 419 | 420 | /** 421 | * togglePause 422 | * 423 | */ 424 | togglePause: function(){ 425 | if(this.ctx.state === 'running') { 426 | this.pause(); 427 | } 428 | else if(this.ctx.state === 'suspended') { 429 | this.resume(); 430 | } 431 | }, 432 | 433 | /** 434 | * Stops playback and internal clock 435 | */ 436 | stop: function(){ 437 | this._clock.stop(); 438 | if (this._audiobed !== false){ 439 | this._audiobed.stop(); 440 | } 441 | for (var kf in this._groupObjPlayers){ 442 | for (var group in this._groupObjPlayers[kf]){ 443 | this._groupObjPlayers[kf][group].stop(); 444 | } 445 | } 446 | for (var kf in this._singleObjAudios){ 447 | for (var idx in this._singleObjAudios[kf]){ 448 | this._singleObjAudios[kf][idx].stop(); 449 | } 450 | } 451 | this.playing = false; 452 | }, 453 | 454 | /** 455 | * Will change the playback position of all single, group and audiobed 456 | * signals. Further, the closes keyframe ahead of the passed time will be 457 | * activated. 458 | * 459 | * @param {float} time - Desired playback position 460 | */ 461 | setTime: function(time, set_audiobed_time=true){ 462 | // activate closest keyframe before time to avoid 463 | // missing / "forgetting" object commands.. 464 | var times = Object.keys(this._keyframes); 465 | 466 | // works even in case the keys are strings 467 | var closest_kf = _.min(times); //Get the lowest numberin case it match nothing. 468 | for(var i = 0; i < times.length; i++){ 469 | if ((times[i] <= time) && (times[i] > closest_kf)){ 470 | closest_kf = times[i]; 471 | } 472 | } 473 | this._handleKeyframe(closest_kf); 474 | 475 | for (var key in this._evts){ 476 | var evt = this._evts[key]; 477 | var evt_time = parseFloat(key); 478 | var newTime = evt_time - time + this.ctx.currentTime; 479 | //console.debug("Evt " + key + " rescheduled from " + evt.deadline + " to " + newTime); 480 | evt.schedule(newTime); 481 | } 482 | 483 | // set single and grouped audio signals to the passed position and 484 | // check if passed time > duration of the single and grouped audio 485 | // signals: 486 | var now = this.ctx.currentTime - this._startTime; 487 | for (var kf in this._singleObjAudios){ 488 | var audioStartPos = parseFloat(kf); 489 | for (var idx in this._singleObjAudios[kf]){ 490 | var duration = this._singleObjAudios[kf][idx].duration; 491 | var audioNewPos = time - audioStartPos; 492 | // negative time values shall stop the signal. 493 | if (audioNewPos <= 0){ 494 | this._singleObjAudios[kf][idx].stop(); 495 | } else { 496 | // should stop audio if audioNewPos > duration 497 | this._singleObjAudios[kf][idx].setTime(audioNewPos); 498 | console.debug("Set audio " + idx + " to position " + audioNewPos); 499 | } 500 | } 501 | } 502 | for (var kf in this._groupObjPlayers){ 503 | var audioStartPos = parseFloat(kf); 504 | for (var group in this._groupObjPlayers[kf]){ 505 | var duration = this._groupObjPlayers[kf][group].duration; 506 | var audioNewPos = time - audioStartPos; 507 | // negative time values shall stop the signal. 508 | if (audioNewPos <= 0){ 509 | this._groupObjPlayers[kf][group].stop(); 510 | } else { 511 | // should stop audio if audioNewPos > duration 512 | this._groupObjPlayers[kf][group].setTime(audioNewPos); 513 | console.debug("Set group " + group + " to position " + audioNewPos); 514 | } 515 | } 516 | } 517 | if ((this._audiobed !== false) && (set_audiobed_time)){ 518 | this._audiobed.setTime(time); 519 | } 520 | }, 521 | 522 | /** 523 | * Toggle panning type between Headphones (binaural) and Stereo rendering 524 | */ 525 | togglePanningType: function(){ 526 | if (this._panningType === "HRTF"){ 527 | this.setPanningType("equalpower"); 528 | this._panningType = "equalpower"; 529 | } else if (this._panningType === "equalpower"){ 530 | this.setPanningType("HRTF"); 531 | this._panningType = "HRTF"; 532 | } 533 | }, 534 | 535 | /** 536 | * @param {("HRTF"|"equalpower")} type - Panning type for all 537 | * objects 538 | */ 539 | setPanningType: function(type){ 540 | for (var key in this.objects){ 541 | this.objects[key].setPanningType(type); 542 | } 543 | this._panningType = type; 544 | }, 545 | 546 | /** 547 | * @returns {("HRTF"|"equalpower")} panningType 548 | */ 549 | getPanningType: function(){ 550 | return this._panningType; 551 | }, 552 | 553 | /** 554 | * Sets listener orientation. Coordinate usage as intended by the Web 555 | * Audio API. See also {@link https://webaudio.github.io/web-audio-api/#the-audiolistener-interface} 556 | * NOTE: This function currently takes only the head rotation but not the 557 | * tilt into account. 558 | * 559 | * @param x 560 | * @param y 561 | * @param z 562 | */ 563 | setListenerOrientation: function(x, y, z){ 564 | this._listenerOrientation = [x, y, z]; 565 | this.ctx.listener.setOrientation(x, y, z, 0, 1, 0); 566 | }, 567 | 568 | /** 569 | * getListenerOrientation 570 | * @returns listenerOrientation 571 | */ 572 | getListenerOrientation: function(){ 573 | return this._listenerOrientation; 574 | }, 575 | 576 | /** 577 | * setListenerPosition 578 | * Coordinate usage as intended by the Web 579 | * Audio API. See also {@link https://webaudio.github.io/web-audio-api/#the-audiolistener-interface} 580 | * @param x 581 | * @param y 582 | * @param z 583 | */ 584 | setListenerPosition: function(x, y, z){ 585 | this._listenerPosition = [x, y, z]; 586 | this.ctx.listener.setPosition(x, y, 0); 587 | }, 588 | 589 | /** 590 | * getListenerPosition 591 | * @returns listenerPosition 592 | */ 593 | getListenerPosition: function(){ 594 | return this._listenerPosition; 595 | }, 596 | 597 | _handleKeyframe: function(key){ 598 | console.debug("Activating keyframe: " + key); 599 | var keyframe = this._keyframes[key]; 600 | //this._kfMapping = {}; 601 | if (this.interactive === false){ 602 | for (var i = 0; i < keyframe.length; i++){ 603 | var triplet = keyframe[i]; 604 | var obj = triplet.obj; 605 | var cmd = triplet.cmd; 606 | var params = triplet.params; 607 | if (cmd === "position"){ 608 | this.objects[obj].setPosition(params); 609 | /** 610 | * Will be fired if object from list gets new Position as per 611 | * the scene data 612 | * @event module:bogJS~ObjectManager#om_newPosition 613 | * @property {string} obj - Name of object 614 | * @property {float[]} pos - New position values as array [x, y, z] 615 | */ 616 | $(this).triggerHandler('om_newPosition', [obj, params]); 617 | } 618 | else if (cmd === "gain"){ 619 | this.objects[obj].setGain(params); 620 | /** 621 | * Will be fired if object from list gets new Gain 622 | * value as per scene data / {@link module:bogJS~ObjectManager#_keyframes} 623 | * @event module:bogJS~ObjectManager#om_newGain 624 | * @property {string} obj - Name of object 625 | * @property {number} gain - New gain value 626 | */ 627 | $(this).triggerHandler('om_newGain', [obj, params]); 628 | } 629 | else if (cmd === "track_mapping"){ 630 | var url = params; 631 | if (url in this._kfMapping === false){ 632 | this._kfMapping[url] = obj; 633 | } 634 | else if ((url in this._kfMapping === true) && (this._kfMapping[url] !== obj)){ 635 | var objs = []; 636 | var alreadyThere = [this._kfMapping[url]]; 637 | this._kfMapping[url] = objs.concat.apply(obj, alreadyThere); 638 | } 639 | } 640 | else if (cmd === "is_present"){ 641 | var state; 642 | if (params === 0) { 643 | state = false; 644 | } else if (params === 1) { 645 | state = true; 646 | } else { 647 | state = params; 648 | } 649 | // Removing as it was never really used and conflicts with switchGroups?? 650 | this.objects[obj].setStatus(state); 651 | /** 652 | * Will be fired if object from list has new State 653 | * @event module:bogJS~ObjectManager#om_isActive 654 | * @property {string} obj - Name of object 655 | * @property {boolean} bool - Bool value if active or not 656 | */ 657 | $(this).triggerHandler('om_isActive', [obj, state]); 658 | } 659 | } 660 | } 661 | this._handleKeyframeAssets(key); 662 | //this._handleKeyframeMappings(); 663 | }, 664 | 665 | _handleKeyframeAssets: function(kf){ 666 | //this._kf_canplay = {}; 667 | if (kf in this._groupObjPlayers){ 668 | for (var group in this._groupObjPlayers[kf]){ 669 | var tmpGrp = this._groupObjPlayers[kf][group]; // TODO: does this cause additional delay? 670 | if (tmpGrp.canplay === false){ 671 | $(tmpGrp).on("audio_loaded", this._loadedStateDelegate(kf, group)); 672 | } 673 | } 674 | } 675 | if (kf in this._singleObjAudios){ 676 | for (var obj in this._singleObjAudios[kf]){ 677 | var tmpAudio = this._singleObjAudios[kf][obj]; // TODO: does this cause additional delay? 678 | if (tmpAudio.canplay === false){ 679 | $(tmpAudio).on("audio_loaded", this._loadedStateDelegate(kf, obj)); 680 | } 681 | } 682 | } 683 | 684 | // now check if all assets are ready for playing: 685 | for (var el in this._kf_canplay[kf]){ 686 | console.debug(el); 687 | if (this._kf_canplay[kf][el] === false){ 688 | console.debug("Pausing playback as not all assets are decoded yet.. "); 689 | this.pause(); 690 | break; 691 | } 692 | } 693 | // if we came to this point: start playback of all keyframe assets 694 | this._startKeyframeAssets(kf); 695 | }, 696 | 697 | _startKeyframeAssets: function(kf){ 698 | if (kf in this._groupObjPlayers){ 699 | for (var group in this._groupObjPlayers[kf]){ 700 | var tmpGrp = this._groupObjPlayers[kf][group]; // TODO: does this cause additional delay? 701 | tmpGrp.play(); 702 | } 703 | } 704 | if (kf in this._singleObjAudios){ 705 | for (var obj in this._singleObjAudios[kf]){ 706 | var tmpAudio = this._singleObjAudios[kf][obj]; // TODO: does this cause additional delay? 707 | tmpAudio.play(); 708 | } 709 | } 710 | }, 711 | 712 | _loadedStateDelegate: function(kf, obj){ 713 | return function(){ 714 | console.debug("Asset now ready: " + obj); 715 | this._kf_canplay[kf][obj] = true; 716 | this._checkLoadedState(kf); 717 | }.bind(this); 718 | }, 719 | 720 | _checkLoadedState: function(kf){ 721 | console.debug(this._kf_canplay[kf]); 722 | for (var obj in this._kf_canplay[kf]) { 723 | if (this._kf_canplay[kf][obj] !== true){ 724 | console.debug("We still need to wait for decoding of asset(s)"); 725 | return; // break loop and return in case any of the objects is not yet ready 726 | } 727 | } 728 | 729 | var first_kf = _.min(Object.keys(this._keyframes)); //Get the first keyframe 730 | if (kf === first_kf){ 731 | $(document).triggerHandler('om_ready'); 732 | } 733 | if (this.ctx.state === "suspended"){ 734 | console.debug("Resuming playback - all assets are decoded now"); 735 | this.resume(); 736 | } 737 | }, 738 | 739 | _handleKeyframeMappings: function(){ 740 | if (JSON.stringify(this._last_kfMapping) !== JSON.stringify(this._kfMapping)){ 741 | console.info("Track mapping has changed" + JSON.stringify(this._kfMapping)); 742 | // Firstly disconnect everything to make sure that no old 743 | // mappings stay connected 744 | // That means that changes have to be made explicitely and 745 | // not implicitely! 746 | for (var key in this._audioInstances){ 747 | this._audioInstances[key].disconnect(); 748 | } 749 | /* 750 | TODO: Irgendwie herausfinden, was sich zum aktuellen Mapping geändert hat. 751 | Dann dementsprechend connecten /disconnecten. 752 | */ 753 | 754 | // And now connect all the mappings as per the keyframe 755 | for (var key in this._kfMapping){ 756 | var pannerObjects = []; 757 | var objs = this._kfMapping[key]; 758 | if (typeof objs === "string"){ // == attribute 759 | pannerObjects = this.objects[objs].highpass; 760 | } 761 | else if (typeof objs === "object"){ // == array 762 | for (var i = 0; i < objs.length; i++){ 763 | console.trace("Adding " + objs[i] + " to the pannerObject array"); 764 | pannerObjects.push(this.objects[objs[i]].highpass); 765 | } 766 | } 767 | this._audioInstances[key].reconnect(pannerObjects); 768 | console.debug("Reconnecting " + key + " with " + objs); 769 | 770 | /** 771 | * Will be fired if track mapping for object from list changes 772 | * @event module:bogJS~ObjectManager#om_newTrackMapping 773 | * @property {string} obj - Name of object 774 | * @property {string[]} objs - Array of to be connected objects 775 | */ 776 | $(this).triggerHandler('om_newTrackMapping', [key, objs]); 777 | } 778 | } 779 | this._last_kfMapping = JSON.parse(JSON.stringify(this._kfMapping)); // making a "copy" and not a reference 780 | }, 781 | 782 | _processCurrentKeyframes: function(){ 783 | for (var key in this._keyframes){ 784 | console.debug("Processing keyframe " + key); 785 | var relTime = parseFloat(this.ctx.currentTime - this._startTime + parseFloat(key)); 786 | this._evts[key] = this._clock.setTimeout(this._buildKeyframeCallback(key, relTime),relTime); 787 | } 788 | }, 789 | 790 | _buildKeyframeCallback: function(key, relTime){ 791 | var that = this; 792 | return function(){ 793 | that._handleKeyframe(key); 794 | that._currentKeyframeIndex = parseFloat(key); 795 | console.debug('Keyframe ' + key + ' reached at context time: ' + relTime); 796 | }; 797 | }, 798 | 799 | /* 800 | update: function(){ 801 | console.trace("Updating scene..") 802 | // neue metadaten lesen 803 | // aktuelle Zeit vom AudioContext holen 804 | // Objekt-Eigenschaften entsprechend ändern 805 | // this.readMetadata(); 806 | // this.processCurrentKeyframes(); 807 | }, 808 | */ 809 | 810 | _checkReadyStart: function(){ 811 | if (this._audiobed !== false){ 812 | return this._audiobed.canplay; 813 | } else { 814 | return true; 815 | } 816 | }, 817 | 818 | 819 | /** 820 | * Sets RollOffFactor for all objects via 821 | * {@link module:bogJS~ObjectController#setRollOffFactor} 822 | * @param factor 823 | */ 824 | setRollOffFactor: function(factor){ 825 | for (var key in this.objects){ 826 | this.objects[key].setRollOffFactor(factor); 827 | } 828 | this._triggerChange(); 829 | }, 830 | 831 | /** 832 | * Sets DistanceModel for all objects via 833 | * {@link module:bogJS~ObjectController#setDistanceModel} 834 | * @param model 835 | */ 836 | setDistanceModel: function(model){ 837 | for (var key in this.objects){ 838 | this.objects[key].setDistanceModel(model); 839 | } 840 | this._triggerChange(); 841 | }, 842 | 843 | /** 844 | * Sets RefDistance for all objects via 845 | * {@link module:bogJS~ObjectController#setRefDistance} 846 | * @param refDistance 847 | */ 848 | setRefDistance: function(refDistance){ 849 | for (var key in this.objects){ 850 | this.objects[key].setRefDistance(refDistance); 851 | } 852 | this._triggerChange(); 853 | }, 854 | 855 | /** 856 | * Sets MaxDistance for all objects via 857 | * {@link module:bogJS~ObjectController#setMaxDistance} 858 | * @param maxDistance 859 | */ 860 | setMaxDistance: function(maxDistance){ 861 | for (var key in this.objects){ 862 | this.objects[key].setMaxDistance(maxDistance); 863 | } 864 | this._triggerChange(); 865 | }, 866 | 867 | setHighpassFreq: function(freq){ 868 | for (var key in this.objects){ 869 | this.objects[key].setHighpassFreq(freq); 870 | } 871 | }, 872 | 873 | _initSwitchGroup: function(groupName){ 874 | var item = this.interactiveInfo.switchGroups[groupName].default; 875 | this.switchGroup(groupName, item); 876 | }, 877 | 878 | switchGroup: function(groupName, item){ 879 | var objects = Object.values(this.interactiveInfo.switchGroups[groupName].items); 880 | for (var obj of objects){ 881 | this.objects[obj].setStatus(false); 882 | } 883 | var active_obj = this.interactiveInfo.switchGroups[groupName].items[item]; 884 | console.info("SwitchGroup " + groupName + " enable " + active_obj); 885 | this.objects[active_obj].setStatus(true); 886 | }, 887 | 888 | setInteractiveGain: function(groupName, dBValue){ 889 | var minLogGain = parseFloat(this.interactiveInfo.gain[groupName].range[0]); 890 | var maxLogGain = parseFloat(this.interactiveInfo.gain[groupName].range[1]); 891 | var gainValue; 892 | if (parseFloat(dBValue) > maxLogGain) { 893 | gainValue = maxLogGain; 894 | } else if(parseFloat(dBValue) < minLogGain) { 895 | gainValue = minLogGain; 896 | } else { 897 | gainValue = dBValue; 898 | } 899 | // Crossfading 900 | //var range = Math.abs(minLogGain) * 0.5 + maxLogGain * 0.5; 901 | var gainGroup = Math.pow(10, (gainValue * 0.5) / 20); 902 | var gainOther = Math.pow(10, ((-1 * gainValue) * 0.5) / 20); 903 | var groupObjects = this.interactiveInfo.gain[groupName].objects; 904 | for (var obj of groupObjects){ 905 | this.objects[obj].setInteractiveGain(gainGroup); 906 | } 907 | // find other objects 908 | var otherObjects = _.difference(Object.keys(this.objects), groupObjects); 909 | for (var oth of otherObjects){ 910 | this.objects[oth].setInteractiveGain(gainOther); 911 | } 912 | console.debug("Set group " + groupName + " gain to " + gainGroup + " and other objects to " + gainOther); 913 | }, 914 | 915 | /** 916 | * @private 917 | * As Chrome (FF works) does not automatically use the new paramters of 918 | * distanceModle, refDistance and maxDistance, we need to trigger a change 919 | * by ourself. The additional value of 0.000001 for x seems to be the 920 | * threshold for Chrome to change the rendering. 921 | */ 922 | _triggerChange: function(){ 923 | var pos = this.getListenerPosition(); 924 | this.setListenerPosition(pos[0] + 0.000001, pos[1], pos[2]); 925 | this.setListenerPosition(pos[0], pos[1], pos[2]); 926 | } 927 | }; 928 | 929 | 930 | 931 | module.exports = ObjectManager; 932 | -------------------------------------------------------------------------------- /src/scene_reader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file scene_reader.js 3 | * @author Michael Weitnauer [weitnauer@irt.de] 4 | */ 5 | 6 | /** 7 | * @module bogJS 8 | */ 9 | 10 | /** 11 | * @callback loaded_callback 12 | */ 13 | 14 | 15 | /** 16 | * Represents SceneReader class. Will load and parse scene data from URL for the 17 | * {@link module:bogJS~ObjectManager|ObjectManager} instance 18 | * 19 | * @constructor 20 | * @abstract 21 | * 22 | * @param {loaded_callback} [loaded_callback=undefined] - Callback that will be executed 23 | * if scene data is loaded and parsed. 24 | * @fires module:bogJS~SceneReader#scene_loaded 25 | * 26 | */ 27 | var SceneReader = function(loaded_callback){ 28 | //this.load(url); 29 | this.callback = loaded_callback || undefined; 30 | } 31 | 32 | SceneReader.prototype = { 33 | 34 | /** 35 | * Executes XHR to load and parse the scene data from the passed URL 36 | * 37 | * @param {string} url - URL to scene data target 38 | * @fires module:bogJS~SceneReader#scene_loaded 39 | */ 40 | load: function(url){ 41 | // we need to do this as within the anonymous success function of the ajax call, 42 | // 'this' will refer to the window object and NOT to the SceneReader instance! 43 | var that = this; 44 | $.ajax({ 45 | type: "GET", 46 | url: url, 47 | dataType: "text", 48 | success: function(text) { 49 | that.parse(text); 50 | if (that.callback !== undefined){ 51 | that.callback.call(); 52 | } 53 | } 54 | }); 55 | }, 56 | 57 | parse: function(rawText) { 58 | var commands = this._tokenize(rawText); 59 | var data = this._parseSpatdif(commands); 60 | var keyframes = data[0]; 61 | var audioURLs = data[1]; 62 | var sceneInfo = data[2]; 63 | var groupObjects = data[3]; 64 | var audiobeds = data[4]; 65 | var extraObjects = data[5]; 66 | var interactiveInfo = data[6]; 67 | var singleObjects = {}; 68 | for (var kf in extraObjects){ 69 | for (var group in groupObjects[kf]){ 70 | for (var el in groupObjects[kf][group]){ 71 | var obj = groupObjects[kf][group][el]; 72 | var idx = extraObjects[kf].indexOf(obj); 73 | console.debug('Checking for double entry for object ' + obj); 74 | if (idx > -1) { 75 | extraObjects[kf].splice(idx, 1); 76 | console.debug('Found group object ' + obj + ' also as single objects entry. Removing if from the list.'); 77 | } 78 | } 79 | } 80 | } 81 | singleObjects = extraObjects; 82 | 83 | /** 84 | * Will be fired if scene data is loaded and parsed 85 | * @event module:bogJS~SceneReader#scene_loaded 86 | * @abstract 87 | * 88 | * @property {module:bogJS~keyframes} keyframes - 'Dictionary' with keyframes 89 | * (Array with commands per detected keyframe in scene) 90 | * 91 | * @property {module:bogJS~audioURLs} audioURLs - 'Dictionary' with audioURLs 92 | * per Object in Scene (to be used for mapping of objects to 93 | * audio signals) 94 | * 95 | * @property {module:bogJS~sceneInfo} sceneInfo - 'Dictionary' with additional sceneInfo 96 | * (Can contain 'name', 'object_count', 'listener_orientation', 97 | * 'listener_position' and / or 'room_dimensions') 98 | * 99 | * @property {module:bogJS~groupObjects} groupObjects - 'Dictionary' 100 | * containing info to identify grouped objects 101 | * 102 | * @property {module:bogJS~singleObjects} singleObjects - 'Dictionary' 103 | * containing info to identify single objects 104 | * 105 | * @property {module:bogJS~audiobeds} audiobeds - 'Dictionary' 106 | * containing objects and their "track mapping" info 107 | * 108 | * @property {module:bogJS~interactiveInfo} interactiveInfo - 'Dictionary' 109 | * containing info for interactive objects and groups 110 | */ 111 | $(this).triggerHandler('scene_loaded', [keyframes, audioURLs, sceneInfo, groupObjects, singleObjects, audiobeds, interactiveInfo]) 112 | }, 113 | 114 | _tokenize: function(d){ 115 | var lines = []; 116 | var data = d.split('\n'); 117 | for (var i = 0; i < data.length; i++){ 118 | if (data[i].indexOf("/spatdif") === 0){ //String.prototype.startsWith() not yet widely supported 119 | var l = {}; 120 | var line = data[i].split(' '); 121 | var command = line[0].split('/'); 122 | l.cmd = command.slice(1, command.length); 123 | l.params = line.slice(1, line.length); 124 | if (l.params.length === 1){ 125 | l.params = l.params[0]; // avoids having an array for a single value 126 | } 127 | lines[lines.length] = l; // makes sure that we append the data at the end and won't skip indices 128 | } 129 | } 130 | return lines; 131 | }, 132 | 133 | _parseSpatdif: function(m){ 134 | var keyframes = {}; 135 | var audioURLs = {}; 136 | var sceneInfo = {}; 137 | var interactiveInfo = {}; 138 | interactiveInfo.switchGroups = {}; 139 | interactiveInfo.gain = {}; 140 | var groups = {}; 141 | var extraObjects = {}; 142 | var audiobeds = {}; 143 | var keyframe = null; 144 | for (var i = 0; i < m.length; i++) { 145 | if (m[i].cmd[0] === "spatdif"){ // darauf verzichten um die lesbarkeit des codes zu verbesern? 146 | if (m[i].cmd[1] === "meta"){ 147 | var meta = m[i]; 148 | if (meta.cmd[3] === "name") { 149 | sceneInfo.name = meta.params; 150 | } else if (meta.cmd[2] === "objects") { 151 | sceneInfo.object_count = meta.params; 152 | } else if ((meta.cmd[2] === "reference") && (meta.cmd[3] === "orientation")){ 153 | sceneInfo.listener_orientation = this._parseFloatArray(meta.params); 154 | } else if ((meta.cmd[2] === "room") && (meta.cmd[3] === "origin")){ 155 | sceneInfo.listener_position = this._parseFloatArray(meta.params); 156 | } else if ((meta.cmd[2] === "room") && (meta.cmd[3] === "dimension")){ 157 | sceneInfo.room_dimensions = this._parseFloatArray(meta.params); 158 | } else if ((meta.cmd[2] === "audiobed") && (meta.cmd[3] === "url")) { 159 | sceneInfo.audiobed_url = meta.params; 160 | } else if ((meta.cmd[2] === "audiobed") && (meta.cmd[3] === "tracks")) { 161 | sceneInfo.audiobed_tracks = meta.params; 162 | } else if (meta.cmd[2] === "interactive") { 163 | if (meta.cmd[3] === "switchGroup") { 164 | if (meta.cmd[4] === "label") { 165 | var label = meta.params[0]; 166 | interactiveInfo.switchGroups[label] = {}; 167 | interactiveInfo.switchGroups[label].default = meta.params[1]; 168 | interactiveInfo.switchGroups[label].items = {}; 169 | } else { 170 | var item_label = meta.params[0]; 171 | interactiveInfo.switchGroups[label].items[item_label] = meta.params[1]; 172 | } 173 | } else if (meta.cmd[3] === "gain"){ 174 | if (meta.cmd[4] === "label") { 175 | var label = meta.params[0]; 176 | interactiveInfo.gain[label] = {}; 177 | interactiveInfo.gain[label].range = [meta.params[1], meta.params[2]]; 178 | interactiveInfo.gain[label].objects = []; 179 | } else { 180 | interactiveInfo.gain[label].objects.push(meta.params); 181 | } 182 | } 183 | } 184 | 185 | } else if (m[i].cmd[1] === "time") { 186 | keyframe = m[i].params; 187 | keyframes[keyframe] = []; 188 | } else if ((m[i].cmd[1] === "source") && (keyframe !== null)) { 189 | // ignore the commands until the first keyframe appears 190 | var obj = m[i].cmd[2]; 191 | var cmd = m[i].cmd[3]; 192 | var params = m[i].params; 193 | 194 | if (cmd === "track_mapping"){ 195 | if ((params.startsWith("bed_")) && (obj in audiobeds === false)){ 196 | audiobeds[obj] = params; 197 | } else if ((params.startsWith("bed_") === false) && (obj in audioURLs === false)) { 198 | audioURLs[obj] = params; 199 | if (keyframe in extraObjects === false){ 200 | extraObjects[keyframe] = []; 201 | } 202 | extraObjects[keyframe].push(obj); 203 | } 204 | } 205 | 206 | if (cmd === "group") { 207 | if (keyframe in groups === false){ 208 | groups[keyframe] = {}; 209 | } 210 | if (params in groups[keyframe] === false){ 211 | groups[keyframe][params] = []; 212 | } 213 | if (groups[keyframe][params].indexOf(obj) === -1){ 214 | groups[keyframe][params].push(obj) // == groups.keyframe.params.push(obj) 215 | console.debug("Adding " + obj + " to group " + params + " at keyframe " + keyframe); 216 | } 217 | } 218 | var triplet = {}; 219 | triplet.obj = obj; 220 | if (cmd === "active"){ 221 | cmd = "is_present"; 222 | } 223 | triplet.cmd = cmd; 224 | triplet.params = m[i].params; 225 | keyframes[keyframe].push(triplet); 226 | } 227 | } 228 | } 229 | return [keyframes, audioURLs, sceneInfo, groups, audiobeds, extraObjects, interactiveInfo]; 230 | }, 231 | 232 | _parseFloatArray: function(array){ 233 | var tmp_array = []; 234 | for (var n in array){ 235 | var number = parseFloat(array[n]); 236 | if (!isNaN(number)){ 237 | tmp_array[tmp_array.length] = number; 238 | } 239 | } 240 | return tmp_array; 241 | } 242 | 243 | } 244 | 245 | 246 | module.exports = SceneReader; 247 | -------------------------------------------------------------------------------- /src/ui.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | /** 3 | * @file ui.js 4 | * @author Michael Weitnauer [weitnauer@irt.de] 5 | */ 6 | 7 | /** 8 | * @module bogJS 9 | */ 10 | 11 | //var _ = require('underscore'); 12 | require('jquery-ui-browserify'); 13 | //require('jquery-ui/ui/widgets/mouse'); 14 | //require('jquery-ui/ui/widgets/draggable'); 15 | require('jquery-mousewheel')($); 16 | require('jquery.transit'); 17 | //var ObjectManager = require('./object_manager'); 18 | 19 | /** 20 | * The UIManager offers some functionality for a classic 2D user interface. Is 21 | * is intended to be heavily improved and abstracted in the future to meet 22 | * requirements of other and general user interfaces. 23 | * 24 | * @constructor 25 | * 26 | * @param {ObjectManager} [om=ObjectManager] - instance of [ObjectManager]{@link 27 | * module:bogJS~ObjectManager} instance 28 | * @param {String} url - URL to scene file. If no ObjectManager instance is 29 | * passed, the URL must be passed. 30 | */ 31 | var UIManager = function(om, url){ 32 | this.om = om || new ObjectManager(url); 33 | this.bg = ""; 34 | this.listener = ""; 35 | this.btn_togglePanning = ""; 36 | this.btn_toggleInteractive = ""; 37 | this.btn_resetOrientation = ""; 38 | this._resizeFactor = 50; 39 | this._iconsize = 32; 40 | this.roomsize = [500, 500]; 41 | this._interactive = false; 42 | this._interval = 0.1; 43 | this._soloed = false; 44 | 45 | this._enableEventListener(); 46 | }; 47 | 48 | UIManager.prototype = { 49 | 50 | /** 51 | * Starts the [ObjectManager]{@link module:bogJS~ObjectManager} and the 52 | * UIManager if the needed ressources are already loaded and decoded. 53 | * 54 | * @returns {Boolean} state - If ObjectManager not yet ready, false will 55 | * be returned. 56 | */ 57 | start: function(){ 58 | if (this.om.start() === true) { 59 | var roomsize = [this.om.roomDimensions[0] * this._resizeFactor, this.om.roomDimensions[1] * this._resizeFactor]; 60 | this._setRoomSize(roomsize); 61 | this._setListenerPosition([this.om._listenerPosition[0], 62 | this.om._listenerPosition[1]]); 63 | this._addObjects(); 64 | var that = this; 65 | if (!this._interactive){ 66 | this._disableInteractive(); 67 | } 68 | $(this.btn_togglePanning).click(function(){ 69 | that.om.togglePanningType(); 70 | $(this).find('img').toggle(); 71 | }); 72 | $(this.btn_toggleInteractive).click(function(){ 73 | that.toggleInteractive(); 74 | $(this).find('img').toggle(); 75 | }); 76 | $(this.btn_resetOrientation).click(function(){ 77 | that.resetDeviceOrientation(); 78 | }); 79 | $(this.listener).mousewheel(function(event, delta){ 80 | var angle = that._xyz2angle(that.om.getListenerOrientation()); 81 | var new_angle = angle; 82 | if(delta < 0) { 83 | new_angle = angle - 5; 84 | } else { 85 | new_angle = angle + 5; 86 | } 87 | that._setListenerOrientation(new_angle); 88 | $('.irt_listener').css({rotate: new_angle}); 89 | return false; // this will prevent window scrolling 90 | }); 91 | return true; 92 | } else { 93 | var that = this; 94 | console.log("Object manager not yet ready.. waiting.."); 95 | } 96 | }, 97 | 98 | /** 99 | * Stops UIManager and ObjectManager 100 | * 101 | */ 102 | stop: function(){ 103 | this.om.stop(); 104 | this._removeObjects(); 105 | $(this.btn_togglePanning).unbind(); 106 | $(this.btn_toggleInteractive).unbind(); 107 | return true; 108 | }, 109 | 110 | /** 111 | * Enables / disables the interactive mode of the UI. 112 | * If interactive mode is enabled, all changes (position, gain, ..) will 113 | * be ignored. Interactivity is offered for positions and soloing of 114 | * objects so far. 115 | */ 116 | toggleInteractive: function(){ 117 | if (this._interactive === false){ 118 | this._enableInteractive(); 119 | } 120 | else if (this._interactive === true){ 121 | this._disableInteractive(); 122 | } 123 | }, 124 | 125 | /** 126 | * Enables the device orientation for rotation on mobiles. Is rather 127 | * untested and may be improved in the future. 128 | * 129 | */ 130 | enableDeviceOrientation: function(){ 131 | var that = this; 132 | if (window.DeviceOrientationEvent) { 133 | this._orientationMode = 'landscape'; 134 | this._angle_offset = 0; 135 | this.angle = 0; 136 | this._last_angle = 180; 137 | this._firstCallFlag = true; 138 | this.onOrientationChange(); 139 | window.addEventListener("orientationchange", this.onOrientationChange.bind(this), false); 140 | window.addEventListener('deviceorientation', function(eventData) { 141 | var raw_angle = -1 * Math.round(eventData.alpha); 142 | this.angle = (raw_angle - this._angle_offset) % 360; 143 | if ((this._orientationMode === 'landscape') && (eventData.gamma < 0)){ 144 | this.angle += 180; 145 | } 146 | if (this._firstCallFlag){ 147 | this.resetDeviceOrientation(); 148 | this._firstCallFlag = false; 149 | } 150 | // the following query should prevent too fast updates of the BRIR change while turning 151 | if ((this._last_angle - this.angle >= 3) || (this._last_angle - this.angle <= -3)){ 152 | this._setListenerOrientation(this.angle); 153 | $(this.listener).css({rotate: this.angle}); 154 | this._last_angle = this.angle; 155 | } 156 | }.bind(this), false); 157 | } else { 158 | console.info("Not supported on your device or browser. Sorry."); 159 | } 160 | }, 161 | 162 | onOrientationChange: function(){ 163 | // Announce the new orientation number 164 | if ((screen.orientation)|| (window.orientation)){ 165 | var orientation = screen.orientation || window.orientation; 166 | var angle = orientation.angle; 167 | if ((angle === 0) || (angle === 180)){ 168 | this._orientationMode = "portrait"; 169 | } else if ((angle === 90) || (angle === 270)) { 170 | this._orientationMode = "landscape"; 171 | } 172 | console.log("Orientation changed to " + this._orientationMode); 173 | } 174 | }, 175 | 176 | resetDeviceOrientation: function(){ 177 | console.log("Resetting Device Orientation!"); 178 | this._angle_offset += this.angle % 360; 179 | this.angle = 0; 180 | this._setListenerOrientation(this.angle); 181 | $(this.listener).css({rotate: this.angle}); 182 | }, 183 | 184 | _enableEventListener: function(){ 185 | $(this.om).on('om_newPosition', function(e, obj, pos){ 186 | this._changeUIObjectPosition(obj, [pos[0], -1 * pos[2]]); 187 | }.bind(this)); 188 | 189 | $(this.om).on('om_isActive', function(e, obj, bool){ 190 | this._displayObject(obj, bool); 191 | }.bind(this)); 192 | 193 | }, 194 | 195 | _displayObject: function(obj, bool){ 196 | if (bool){ 197 | $("#" + obj).show(); 198 | } else{ 199 | $("#" + obj).hide(); 200 | } 201 | console.debug("Setting state of object " + obj + ' to ' + bool); 202 | }, 203 | 204 | _enableInteractive: function(){ 205 | for (var key in this.om.objects){ 206 | $("#"+key).draggable('enable'); 207 | $("#"+key).hover(function() { 208 | $(this).css("cursor","move"); 209 | }); 210 | } 211 | this._enableSolo(); 212 | $(this.om).off('om_newPosition'); 213 | $(this.om).off('om_isActive'); 214 | this._interactive = true; 215 | this.om.interactive = true; 216 | }, 217 | 218 | _disableInteractive: function(){ 219 | for (var key in this.om.objects){ 220 | $("#"+key).draggable('disable'); 221 | $("#"+key).off('dblclick'); 222 | $("#"+key).hover(function() { 223 | $(this).css("cursor","auto"); 224 | }); 225 | } 226 | this._enableEventListener(); 227 | this._interactive = false; 228 | this.om.interactive = false; 229 | }, 230 | 231 | _enableSolo: function(){ 232 | var that = this; 233 | for (var key in this.om.objects){ 234 | $("#"+key).dblclick(function(event, ui){ 235 | if (that._soloed !== event.target.id){ 236 | for (var key in that.om.objects){ 237 | console.debug("Muting " + key); 238 | $("#"+key).addClass("irt_object_disabled"); 239 | that.om.objects[key].setStatus(false); 240 | } 241 | $("#"+event.target.id).removeClass("irt_object_disabled"); 242 | console.debug("Unmuting " + event.target.id); 243 | that.om.objects[event.target.id].setStatus(true); 244 | that._soloed = event.target.id; 245 | } else { 246 | for (var obj in that.om.objects){ 247 | console.debug("Unmuting " + obj); 248 | $("#"+obj).removeClass("irt_object_disabled"); 249 | that.om.objects[obj].setStatus(true); 250 | } 251 | that._soloed = false; 252 | } 253 | }); 254 | } 255 | }, 256 | 257 | _addObjects: function(){ 258 | for (var key in this.om.objects){ 259 | $(this.bg).append("
"); 260 | $("#"+key).append("

" + key + "

"); 261 | 262 | var pos = this.om.objects[key].getPosition(); 263 | this._changeUIObjectPosition(key, [pos[0], -1 * pos[2]]); 264 | var that = this; 265 | $("#"+key).draggable({ 266 | drag: _.throttle( 267 | function(event, ui){ 268 | var topleft = [ui.position.top, ui.position.left]; 269 | var xy = that._topleft2xy(topleft); 270 | that.om.objects[event.target.id].setPosition([xy[0], 0, -1 * xy[1]]); 271 | console.debug("Drag event position: " + topleft); 272 | }, 273 | 50) 274 | }); 275 | if (!this.om.objects[key].getStatus()){ 276 | this._displayObject(key, false); 277 | } 278 | } 279 | }, 280 | 281 | _removeObjects: function(){ 282 | for (var key in this.om.objects){ 283 | $("#"+key).remove(); 284 | } 285 | }, 286 | 287 | _setRoomSize: function(roomsize) { 288 | $(this.bg).css({"width": roomsize[0], "height": roomsize[1]}); 289 | this.roomsize = roomsize; 290 | }, 291 | 292 | _setListenerPosition: function(xy) { 293 | var topleft = this._xy2topleft(xy); 294 | $(this.listener).css({"top": topleft[0], "left": topleft[1]}); 295 | console.info("New listener position: " + topleft); 296 | var that = this; 297 | $(this.listener).draggable({ 298 | drag: function(event, ui){ 299 | var topleft = [ui.position.top, ui.position.left]; 300 | var xy = that._topleft2xy(topleft); 301 | that.om.setListenerPosition(xy[0], xy[1], 0); 302 | } 303 | }); 304 | }, 305 | 306 | _setListenerOrientation: function (angle){ 307 | // As x and y are somehow flipped, x needs to be calculated with sinus 308 | // and not with cosinus.. TODO: check why !? 309 | //var x = 32 * Math.sin(angle * (Math.PI / 180)); 310 | //var y = 32 * Math.cos(angle * (Math.PI / 180)); 311 | var x = 10 * Math.sin(angle * (Math.PI / 180)); 312 | var y = 0; // as we don't have a lattitude here y is always 0 :) 313 | var z = -10 * Math.cos(angle * (Math.PI / 180)); 314 | 315 | console.info("Set angle " + angle + " to new listener orientation " + x + " " + y + " " + z); 316 | this.om.setListenerOrientation(x, y, z); 317 | }, 318 | 319 | _changeUIObjectPosition: function(id, xy) { 320 | var topleft = this._xy2topleft(xy); 321 | $("#"+id).css({"top": topleft[0], "left": topleft[1]}); 322 | console.debug("New position of " + id + " is: " + topleft + "(xy: " + xy + ")"); 323 | }, 324 | 325 | _setObjPos: function(id, topleft){ 326 | var xy = this._topleft2xy(topleft); 327 | var xyz = [xy[0], xy[1], 0]; 328 | this.om.objects[id].setPostion(xyz); 329 | }, 330 | 331 | _xy2topleft: function(xy){ 332 | var topleft = [(-xy[1] * this._resizeFactor) + this.roomsize[1] / 2, 333 | (xy[0] * this._resizeFactor) + this.roomsize[0] / 2]; 334 | return topleft; 335 | }, 336 | 337 | _topleft2xy: function(topleft){ 338 | var xy = [-(this.roomsize[1] / 2 - topleft[1]) / this._resizeFactor, 339 | (this.roomsize[0] / 2 - topleft[0]) / this._resizeFactor]; 340 | return xy; 341 | }, 342 | 343 | _angle2xy: function(angle){ 344 | // For some reason, x needs to be calculated with sinus 345 | // and not with cosinus.. TODO: check why !? 346 | var x = 10 * Math.sin(angle * (Math.PI / 180)); 347 | var y = 0; 348 | var z = -10 * Math.cos(angle * (Math.PI / 180)); 349 | return [x, z]; 350 | }, 351 | 352 | _xyz2angle: function(xyz){ 353 | var angle = Math.atan2(xyz[0], -xyz[2]) / Math.PI * 180; 354 | return angle; 355 | } 356 | }; 357 | 358 | module.exports = UIManager; 359 | -------------------------------------------------------------------------------- /tools/Multichannel-Order_Browsertest.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IRT-Open-Source/bogJS/5059ac3f212a34c43b8235ef9d79f5b7831d2480/tools/Multichannel-Order_Browsertest.xlsx -------------------------------------------------------------------------------- /tools/create_channelOrder_testfiles.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p tmp_wav 4 | 5 | declare -a frqs=(100.0 500.0 1000.0 2000.0 3000.0 4000.0 5000.0 6000.0 7000.0 8000.0 9000.0 10000.0 11000.0) 6 | 7 | chs=$1 8 | chs_internal=`expr $chs - 1` 9 | 10 | # Generate wav files with spoken trial and condition numbers 11 | for number in `seq 0 $chs_internal`; do 12 | sox -b 16 -n tmp_wav/"ch"$number".wav" synth 1.0 sin ${frqs[$number]} gain -9 13 | printf "." 14 | done 15 | 16 | printf "\n" 17 | 18 | 19 | new_wav=signals/order/$chs"chs".wav 20 | mp4_name=signals/order/`basename "$new_wav" .wav`.mp4 21 | opus_name=signals/order/`basename "$new_wav" .wav`.opus 22 | 23 | # Now merge all sinus wav files 24 | sox -b 16 -M tmp_wav/*.wav $new_wav 25 | 26 | 27 | if [ $chs -lt 3 ] 28 | then 29 | ffmpeg -i $new_wav -c:a libfdk_aac -cutoff 20000 $mp4_name 30 | elif [ $chs -eq 3 ] 31 | then 32 | afconvert -f mp4f -d aac@48000 -c 3 -l AAC_3_0 $new_wav -o $mp4_name 33 | elif [ $chs -eq 4 ] 34 | then 35 | afconvert -f mp4f -d aac@48000 -c 4 -l AAC_Quadraphonic $new_wav -o $mp4_name 36 | elif [ $chs -eq 5 ] 37 | then 38 | afconvert -f mp4f -d aac@48000 -c 5 -l AAC_5_0 $new_wav -o $mp4_name 39 | elif [ $chs -eq 6 ] 40 | then 41 | afconvert -f mp4f -d aac@48000 -c 6 -l AAC_6_0 $new_wav -o $mp4_name 42 | elif [ $chs -eq 7 ] 43 | then 44 | afconvert -f mp4f -d aac@48000 -c 7 -l AAC_7_0 $new_wav -o $mp4_name 45 | elif [ $chs -eq 8 ] 46 | then 47 | afconvert -f mp4f -d aac@48000 -c 8 -l AAC_Octagonal $new_wav -o $mp4_name 48 | else 49 | echo "=======================================================" 50 | echo "aac encoding not possible for this channel number" 51 | echo "=======================================================" 52 | fi 53 | 54 | 55 | oggenc $new_wav --advanced-encode-option disable_coupling=1 56 | 57 | # as the current version of opus-tools (0.1.9) does no more support the 58 | # uncoupled flag, we switched to the last working version (0.1.6) but using 59 | # the current libopus version (1.1.1). Moreover, we prevent updates for 60 | # opus-tools by "pinning" it with homebrew. You might want to "unpin" the 61 | # formula by executing 62 | # brew unpin opus-tools 63 | # NOTE: In case you might get into trouple in the future: http://stackoverflow.com/questions/3987683/homebrew-install-specific-version-of-formula 64 | opusenc --uncoupled $new_wav $opus_name 65 | 66 | 67 | 68 | #ffmpeg -i sync_test.mp4 -i $mp4_name -c:v copy -c:a copy -bsf:a aac_adtstoasc `basename "$new_wav" .wav`_vid.mp4 69 | #ffmpeg -i sync_test.webm -i $opus_name -c:v copy -c:a copy `basename "$new_wav" .wav`_vid.webm 70 | 71 | rm -r tmp_wav 72 | -------------------------------------------------------------------------------- /tools/encodeMultichannel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Required steps to make this script running (tested on OS X 10.11.2): 4 | # brew install opus-tools vorbis-tools 5 | # cd $( brew --prefix ) 6 | # brew unlink opus-tools 7 | # brew git checkout c2e5077062344a1ffb90f0b871bc574227e7790d Library/Formula/opus-tools.rb 8 | # OR 9 | # brew install https://raw.githubusercontent.com/kickermeister/homebrew-versions/patch-1/opus-tools.rb 10 | # brew install opus-tools 11 | # brew pin opus-tools 12 | 13 | chs=$1 14 | chs_internal=`expr $chs - 1` 15 | file=$2 16 | 17 | mp4_name=`basename "$file" .wav`.mp4 18 | opus_name=`basename "$file" .wav`.opus 19 | 20 | # Now merge all sinus wav files 21 | #sox -b 16 $file $file 22 | 23 | 24 | if [ $chs -lt 3 ] 25 | then 26 | ffmpeg -i $file -c:a libfdk_aac -cutoff 20000 $mp4_name 27 | elif [ $chs -eq 3 ] 28 | then 29 | afconvert -f mp4f -d aac@48000 -c 3 -l AAC_3_0 $file -o $mp4_name 30 | elif [ $chs -eq 4 ] 31 | then 32 | afconvert -f mp4f -d aac@48000 -c 4 -l AAC_Quadraphonic $file -o $mp4_name 33 | elif [ $chs -eq 5 ] 34 | then 35 | afconvert -f mp4f -d aac@48000 -c 5 -l AAC_5_0 $file -o $mp4_name 36 | elif [ $chs -eq 6 ] 37 | then 38 | afconvert -f mp4f -d aac@48000 -c 6 -l AAC_6_0 $file -o $mp4_name 39 | elif [ $chs -eq 7 ] 40 | then 41 | afconvert -f mp4f -d aac@48000 -c 7 -l AAC_7_0 $file -o $mp4_name 42 | elif [ $chs -eq 8 ] 43 | then 44 | afconvert -f mp4f -d aac@48000 -c 8 -l AAC_Octagonal $file -o $mp4_name 45 | else 46 | echo "=======================================================" 47 | echo "aac encoding not possible for this channel number" 48 | echo "=======================================================" 49 | fi 50 | 51 | 52 | oggenc $file --advanced-encode-option disable_coupling=1 53 | 54 | # as the current version of opus-tools (0.1.9) does no more support the 55 | # uncoupled flag, we switched to the last working version (0.1.6) but using 56 | # the current libopus version (1.1.2). This version supports the uncoupled 57 | # flag, but does not advertise it. 58 | # 59 | # This can be done by using an old Formula of opus-tools (0.1.6): 60 | # git checkout c2e5077062344a1ffb90f0b871bc574227e7790d /usr/local/Library/Formula/opus-tools.rb 61 | # 62 | # Moreover, we prevent updates for 63 | # opus-tools by "pinning" it with homebrew. You might want to "unpin" the 64 | # formula by executing 65 | # brew unpin opus-tools 66 | # NOTE: In case you might get into trouple in the future: http://stackoverflow.com/questions/3987683/homebrew-install-specific-version-of-formula 67 | opusenc --uncoupled $file $opus_name 68 | 69 | 70 | 71 | #ffmpeg -i sync_test.mp4 -i $mp4_name -c:v copy -c:a copy -bsf:a aac_adtstoasc `basename "$file" .wav`_vid.mp4 72 | #ffmpeg -i sync_test.webm -i $opus_name -c:v copy -c:a copy `basename "$file" .wav`_vid.webm 73 | 74 | -------------------------------------------------------------------------------- /tools/zip_demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | zip bogJS_demo.zip ../css/* \ 4 | ../css/images/* \ 5 | ../fonts/* \ 6 | ../img/* \ 7 | ../demos/360/index.html \ 8 | ../demos/360/valiant360/css/valiant360.css \ 9 | ../demos/360/valiant360/jquery.valiant360.js \ 10 | ../demos/360/valiant360/js/three.min.js \ 11 | ../dist/bogJS-latest.* \ 12 | ../dist/bogJS-dev.* \ 13 | ../scenes/romeo_XYZconverted.spatdif \ 14 | ../scenes/debo.spatdif \ 15 | ../scenes/LongTrainRunning_short.spatdif \ 16 | ../scenes/Vulkane360_XYZconverted.spatdif \ 17 | -X -x *.wav .git* ../_site/ ../out/ ../doc/ zip_demo.sh bogJS_demo/ 18 | 19 | 20 | #tar --exclude='*.wav' \ 21 | # --exclude='.git*' \ 22 | # --exclude='_site/' \ 23 | # --exclude='out/' \ 24 | # --exclude='zip_demo.sh' \ 25 | # --exclude='index.html.developing' \ 26 | # -cvzf bogJS_demo.tar.gz css/* \ 27 | # css/images/* \ 28 | # fonts/* \ 29 | # img/* \ 30 | # js/* \ 31 | # scenes/romeo.spatdif \ 32 | # scenes/LongTrainRunning_short.spatdif \ 33 | # signals/romeo/* \ 34 | # signals/LongTrainRunning/* \ 35 | # ./* \ 36 | # ../html5_player/script/irtPlayer_new.js 37 | 38 | 39 | --------------------------------------------------------------------------------