diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..275f3c5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + "version": "0.2.0", + "configurations": [{ + "type": "node", + "request": "launch", + "name": "Electron: Main", + "protocol": "inspector", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "runtimeArgs": [ + "--remote-debugging-port=9223", + "." + ], + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" + } + }, + { + "name": "Electron: Renderer", + "type": "chrome", + "request": "attach", + "port": 9223, + "webRoot": "${workspaceFolder}", + "timeout": 30000 + } + ], + "compounds": [{ + "name": "Electron: All", + "configurations": [ + "Electron: Main", + "Electron: Renderer" + ] + }] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b0aa282..9e87f70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,175 +1,206 @@ # Change Log + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [0.5.0] - XXXX-XX-XX +## [0.5.0] - 2019-01-13 + ### Added -- Panic switch (Global stop) by pressing ESCAPE -- Solo sound options - choose to auto-stop playing sounds when a new sound starts -- Volume control for individual sounds -- Double-click volume bar to reset to 100% -- Show changelog on first time after update + +- Panic switch (Global stop) by pressing ESCAPE +- Solo sound options - choose to auto-stop playing sounds when a new sound starts +- Volume control sound in sound settings +- Double-click volume bar to reset to 100% +- Show changelog on first time after update ### Fixed -- Fixed issue with playlist not switching song after fade out -- Fixed issue with loaded bar not hiding when errors loading songs -- Removed missed restricted characters from song ids -- General refactoring and cleaning of code + +- Fixed issue with playlist not switching song after fade out +- Fixed issue with loaded bar not hiding when errors loading songs +- Removed missed restricted characters from song ids +- General refactoring and cleaning of code ### Changed -- Name (Wowee!) -- Misc fonts/logos related to new name -- JEditable updated to v2.02 -- Removed dragging of keys -- Enlarged the playlist box vertically -- Upgraded to Interact.js v1.3.3 -- Upgraded Howler.js to v2.0.10 -- Updated npm package dependencies +- Name (Wowee!) +- Misc fonts/logos related to new name +- JEditable updated to v2.02 +- Removed dragging of keys +- Enlarged the playlist box vertically +- Upgraded to Interact.js v1.3.3 +- Upgraded Howler.js to v2.0.10 +- Updated npm package dependencies ## [0.4.1] - 2017-05-25 + ### Added -- Playlist: Dropdown on playlist header to access action items -- Playlist: Can set initial order via playlist dropdown -- Playlist: Can reset to initial order via dropdown -- Playlist: Empty Playlist now has confirmation dialogue + +- Playlist: Dropdown on playlist header to access action items +- Playlist: Can set initial order via playlist dropdown +- Playlist: Can reset to initial order via dropdown +- Playlist: Empty Playlist now has confirmation dialogue ### Fixed -- Fade times now work properly with 0 seconds -- Playlist settings no longer stored after dragging/drop sounds within playlist -- Reduced playlist animation time to improve performance + +- Fade times now work properly with 0 seconds +- Playlist settings no longer stored after dragging/drop sounds within playlist +- Reduced playlist animation time to improve performance ### Changed -- Settings menu: Visual redesign -- Settings menu: Saves when exiting (no need to save explicitly anymore) -- Updated Sortable.js to v1.5.1 + +- Settings menu: Visual redesign +- Settings menu: Saves when exiting (no need to save explicitly anymore) +- Updated Sortable.js to v1.5.1 ## [0.4.0] - 2017-05-07 + ### Added -- Sound Settings: Set/remove 'Played' status -- General Settings: Mark sounds as played (or not) -- Pages: Page names and page fade in/out times by right/ctrl-clicking on tab -- Pages: Scroll on page tabs to get overflow pages -- Menu: Tutorial option that links to YouTube video + +- Sound Settings: Set/remove 'Played' status +- General Settings: Mark sounds as played (or not) +- Pages: Page names and page fade in/out times by right/ctrl-clicking on tab +- Pages: Scroll on page tabs to get overflow pages +- Menu: Tutorial option that links to YouTube video ### Fixed -- Sound settings "cannot read proprty 'name' of undefined" error -- Deleting sound "cannot read property '0' of undefined" error -- Deleting sound removes playing and soundNotLoaded classes -- Waveform shows playhead at proper location after loading waveform + +- Sound settings "cannot read proprty 'name' of undefined" error +- Deleting sound "cannot read property '0' of undefined" error +- Deleting sound removes playing and soundNotLoaded classes +- Waveform shows playhead at proper location after loading waveform ## [0.3.2] - 2017-04-22 + ### Fixed -- Playlist: Space key doesn't create error when no sounds in playlist -- Playlist: Sounds dragged out of top spot no longer stay yellow -- Playlist: Search doc error fixed + +- Playlist: Space key doesn't create error when no sounds in playlist +- Playlist: Sounds dragged out of top spot no longer stay yellow +- Playlist: Search doc error fixed ### Changed -- Opening sound options no longer loads the waveform -- Playlist: new sounds are added above already played sounds -- Playlist: When loading multiple sounds, waveform of first sound shows -- Keyboard: When loading multiple sounds, waveform of first sound shows + +- Opening sound options no longer loads the waveform +- Playlist: new sounds are added above already played sounds +- Playlist: When loading multiple sounds, waveform of first sound shows +- Keyboard: When loading multiple sounds, waveform of first sound shows ## [0.3.1] - 2017-04-08 + ### Added -- OS X build + +- OS X build ### Changed -- Updated interact.js to v1.2.8 (solved darwin issues) -- Backspace also deletes keys (solved darwin issues) + +- Updated interact.js to v1.2.8 (solved darwin issues) +- Backspace also deletes keys (solved darwin issues) ### Fixed -- Removed error when clicking on key with no sound -- Loading bar is removed if 0 sounds -- Copy/paste on darwin -- Temp fix for random waveformedInfo seek error after ctrl/cmd+X -- Pasting a song removes 'played' status + +- Removed error when clicking on key with no sound +- Loading bar is removed if 0 sounds +- Copy/paste on darwin +- Temp fix for random waveformedInfo seek error after ctrl/cmd+X +- Pasting a song removes 'played' status ## [0.3.0] - 2017-04-07 + ### Added -- Pause/stop option in General Settings -- Global Fade in and Fade out times -- Pressing 'ENTER' in playlist search un-focuses it -- Double click on waveform to play sound from that spot -- COPY/CUT/PASTE shortcuts to move keys around keyboards/pages -- Import multiple sounds at a time -- Mac build scripts -- Icons/background for Mac builds -- Page names have associated hotkey in name -- Sound resets after changing waveform region + +- Pause/stop option in General Settings +- Global Fade in and Fade out times +- Pressing 'ENTER' in playlist search un-focuses it +- Double click on waveform to play sound from that spot +- COPY/CUT/PASTE shortcuts to move keys around keyboards/pages +- Import multiple sounds at a time +- Mac build scripts +- Icons/background for Mac builds +- Page names have associated hotkey in name +- Sound resets after changing waveform region ### Changed -- License in "About" to Apache 2.0 -- Order of menu items -- Sounds now "pause" by default (not stop) -- Minor color/visual fixes -- Removed start/end from sound settings -- Can select unloaded keys -- Cannot launch sounds when holding CONTROL -- howls are not saved in json - fixed click-key-before-anything-else tracking weirdness (was because playState wasn't always null on launch) -- Switched to Howler.js for sound engine -- Removed looping, for now -- Added text-shadow to emphasize key text -- Updated Materialize to v0.98.1 -- Updated jQuery to v3.2.1 -- Updated Wavesurfer to v1.3.7 -- Updated Electron to 1.6.4 + +- License in "About" to Apache 2.0 +- Order of menu items +- Sounds now "pause" by default (not stop) +- Minor color/visual fixes +- Removed start/end from sound settings +- Can select unloaded keys +- Cannot launch sounds when holding CONTROL +- howls are not saved in json - fixed click-key-before-anything-else tracking weirdness (was because playState wasn't always null on launch) +- Switched to Howler.js for sound engine +- Removed looping, for now +- Added text-shadow to emphasize key text +- Updated Materialize to v0.98.1 +- Updated jQuery to v3.2.1 +- Updated Wavesurfer to v1.3.7 +- Updated Electron to 1.6.4 ### Fixed -- Settings now saving properly -- Sounds are marked as played when they end -- Playlist selects first sound after last one stops -- Empty playlist creates empty object and saves it to the json -- Can have periods and square brackets in sound names -- Sounds that don't load are correctly styled -- Deleting a song no longer breaks waveform sound name -- Deleting a song removed played class on key -- Empty playlist search error fixed -- Playlist songs playing that aren't on top are now stopped instead of top one playing -- 3D key box-shadow changes color properly -- Settings now updates with new properties properly -- Can have multiple of same sound in playlist -- Waveforms are not loaded when stopping or pausing a sound + +- Settings now saving properly +- Sounds are marked as played when they end +- Playlist selects first sound after last one stops +- Empty playlist creates empty object and saves it to the json +- Can have periods and square brackets in sound names +- Sounds that don't load are correctly styled +- Deleting a song no longer breaks waveform sound name +- Deleting a song removed played class on key +- Empty playlist search error fixed +- Playlist songs playing that aren't on top are now stopped instead of top one playing +- 3D key box-shadow changes color properly +- Settings now updates with new properties properly +- Can have multiple of same sound in playlist +- Waveforms are not loaded when stopping or pausing a sound ## [0.2.0] - 2017-01-28 + ### Added -- Search function for playlist -- Functioning/saving settings menu - * Playlist: Songs to the bottom after playing - * Playlist: Songs delete after playing -- Dragging keys around on the same page (between pages to come!) -- Automatically checks for new version on startup + +- Search function for playlist +- Functioning/saving settings menu + - Playlist: Songs to the bottom after playing + - Playlist: Songs delete after playing +- Dragging keys around on the same page (between pages to come!) +- Automatically checks for new version on startup ### Fixed -- Won't try to play songs if they're all filtered out -- No more error on cancelling browse in sound settings -- Waveforms now track all the time (no need to sneak up) -- Inputs won't trigger sounds AT ALL + +- Won't try to play songs if they're all filtered out +- No more error on cancelling browse in sound settings +- Waveforms now track all the time (no need to sneak up) +- Inputs won't trigger sounds AT ALL ### Changed -- Storing data is now JSON in appData/data/ + +- Storing data is now JSON in appData/data/ ## [0.1.1] - 2017-01-21 + ### Added -- Restart option to menu -- Global settings (not working yet though) -- Error dialog (so errors show to user, not just in devTools) -- infoObj property to sounds ('key' or 'playlist') -- Screenshot to ReadMe + +- Restart option to menu +- Global settings (not working yet though) +- Error dialog (so errors show to user, not just in devTools) +- infoObj property to sounds ('key' or 'playlist') +- Screenshot to ReadMe ### Fixed -- Color setting is hidden for playlist items -- App quits when window is closed -- Errors no longer thrown for trying to play/open settings on key with no sound -- Cleaned ' and , out of sound ids (was breaking playlist) + +- Color setting is hidden for playlist items +- App quits when window is closed +- Errors no longer thrown for trying to play/open settings on key with no sound +- Cleaned ' and , out of sound ids (was breaking playlist) ### Changed -- Sound names on keys are slightly smaller -- HTML gets app version from package.json -- ReadMe information update -- More thoroughly documented previous code + +- Sound names on keys are slightly smaller +- HTML gets app version from package.json +- ReadMe information update +- More thoroughly documented previous code ## [0.1.0] - 2017-01-11 -- init + +- init diff --git a/index.js b/index.js index ba4f469..07dca8c 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,18 @@ /*jshint esversion: 6 */ const electron = require("electron"); -const {app, BrowserWindow, dialog} = electron; +const { + app, + BrowserWindow, + dialog +} = electron; -app.on('ready', function() { +app.on('ready', function () { var win = new BrowserWindow(); win.maximize(); win.loadURL('file://' + __dirname + '/src/main.html'); - win.openDevTools(); + //win.openDevTools(); }); -app.on('window-all-closed', function() { +app.on('window-all-closed', function () { app.quit(); -}); +}); \ No newline at end of file diff --git a/package.json b/package.json index 98de646..9a83d21 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "electron": "1.8.6", "electron-builder": "^10.17.3", "electron-builder-squirrel-windows": "^11.5.0", - "gh-release": "^2.2.0" + "gh-release": "^2.2.0", + "npm": "^5.8.0" }, "dependencies": { "circular-json": "^0.3.1", @@ -39,7 +40,6 @@ "fs-jetpack": "^0.10.5", "gh-latest-release": "^1.0.0", "marked": "^0.3.19", - "materialize-css": "^1.0.0-rc.1", - "npm": "^5.8.0" + "materialize-css": "^1.0.0-rc.1" } -} +} \ No newline at end of file diff --git a/src/scripts/events.js b/src/scripts/events.js index a8ea3e2..7488e7f 100644 --- a/src/scripts/events.js +++ b/src/scripts/events.js @@ -5,9 +5,6 @@ */ /*jshint esversion: 6 */ -// Required js scripts -const soundInfoManager = require("./soundInfoManager"); - var settingsInfoObj; // Either keyInfo or playlistInfo, whichever has the sound being changed in settings var specialKeys = ['MINUS', 'EQUALS', 'OPEN_BRACKET', 'CLOSE_BRACKET', 'SEMICOLON', 'QUOTE', 'BACK_SLASH', 'BESIDE_Z', 'COMMA', 'PERIOD', 'SLASH']; var playlistPlayingSound = { @@ -23,25 +20,26 @@ var curVol; function setKeyEvents() { // Handles when a file is dropped on a key - keys.on('drop', function(e) { + keys.on('drop', function (e) { e.originalEvent.preventDefault(); // Prevent default action - try{ + try { // grab the id of the target key var siblingCount = -1; var newSoundInfo; var first = true; for (let f of e.originalEvent.dataTransfer.files) { var targetKey = $(e.target); - if(first){ + if (first) { first = targetKey.attr('id'); } - if(siblingCount > -1){ - if(siblingCount < targetKey.nextAll().length) - targetKey = $(e.target).nextAll().eq(siblingCount); + if (siblingCount > -1) { + if (siblingCount < targetKey.nextAll().length) + targetKey = $(e.target).nextAll().eq(siblingCount); } var id = targetKey.attr('id'); // Create a new sound info object - newSoundInfo = soundInfoManager.createNewSoundInfo(f.path, id); + newSoundInfo = new sounds.SoundInfo(f.path, id); + sounds.createNewHowl(newSoundInfo); // Store the new sound info object in the keyInfo object keyInfo[id] = newSoundInfo; targetKey.find('.audioName').text(newSoundInfo.name); @@ -52,35 +50,36 @@ function setKeyEvents() { pagesInfo['page' + currentPage].keyInfo = keyInfo; storage.storeObj("pagesInfo", pagesInfo); waveforms.load(keyInfo[first]); - } - catch(err){} + } catch (err) {} return false; }); - keys.on('dragover', function(e) { + keys.on('dragover', function (e) { //$('#' + e.target.id).css("box-shadow", "0px 0px 4px 4px rgba(255,247,99,1)"); return false; }); - keys.on('dragleave', function(e) { + keys.on('dragleave', function (e) { //$('#' + e.target.id).css("box-shadow", "0px 0px"); return false; }); // File is dropped onto playlist box - register and add info - $('.playlistBox').on('drop', function(e) { + $('.playlistBox').on('drop', function (e) { e.originalEvent.preventDefault(); // Prevent default action var first = true; for (let f of e.originalEvent.dataTransfer.files) { // Create new soundInfo object - var newSoundInfo = soundInfoManager.createNewSoundInfo(f.path); + newSoundInfo = new sounds.SoundInfo(f.path); + console.log(newSoundInfo); + sounds.createNewHowl(newSoundInfo); playlistInfo[newSoundInfo.id] = newSoundInfo; view.createPlaylistItem(newSoundInfo); // Create a new li in the playlist - if(first){ + if (first) { waveforms.load(newSoundInfo); first = false; } } - if(e.originalEvent.dataTransfer.files.length > 0){ + if (e.originalEvent.dataTransfer.files.length > 0) { storage.storeObj("playlistInfo", playlistInfo); } updatePlaylistClickFunctions(); // Ensure new songs react properly to clicking @@ -88,7 +87,7 @@ function setKeyEvents() { }); // Click on keyboard key - $('.btn-key').on('click', function(e) { + $('.btn-key').on('click', function (e) { clickSound(e, keyInfo); }); @@ -96,17 +95,18 @@ function setKeyEvents() { function clickSound(e, infoObj) { var id = e.target.id; if (!infoObj.hasOwnProperty(id)) { - infoObj[id] = {}; infoObj[id].id = id; + infoObj[id] = {}; + infoObj[id].id = id; storage.checkAgainstDefault(infoObj[id], 'soundInfo'); } waveforms.track(infoObj[id]); } // Right click to bring up settings and populate them - keys.on('contextmenu', function(e) { + keys.on('contextmenu', function (e) { var key = e.target.id; settingsInfoObj = keyInfo; - if(keyInfo.hasOwnProperty(key)){ + if (keyInfo.hasOwnProperty(key)) { settings.openSoundSettings(keyInfo[key]); //waveforms.load(keyInfo[key]); } @@ -115,11 +115,11 @@ function setKeyEvents() { // Set functions when clicking on playlist sounds function updatePlaylistClickFunctions() { // Click on playlist sound -> load waveform - $('.playlistSound').on('click', function(e) { + $('.playlistSound').on('click', function (e) { clickSound(e, playlistInfo); }); // Right-click on playlist sound -> open sound settings - $('#playlist-songs li').on('contextmenu', function(e) { + $('#playlist-songs li').on('contextmenu', function (e) { var id = e.target.id; settingsInfoObj = playlistInfo; settings.openSoundSettings(playlistInfo[id]); @@ -129,11 +129,11 @@ function setKeyEvents() { updatePlaylistClickFunctions(); // Handles pressing a real key anywhere on the page - $(document).keydown(function(e) { + $(document).keydown(function (e) { var key = keyboardMap[e.which]; var code = e.which; - if(key === 'CONTROL'){ + if (key === 'CONTROL') { ctrl = true; $('#waveform').css('pointer-events', 'none'); } @@ -144,7 +144,7 @@ function setKeyEvents() { if ((code > 64 && code < 91) || (code > 47 && code < 58) || ($.inArray(key, specialKeys) > -1)) { // Check if the sound was loaded or not, and if it even exists key = 'page' + currentPage + '_' + key; - if(keyInfo.hasOwnProperty(key)){ + if (keyInfo.hasOwnProperty(key)) { if (!$("#" + key).parent().hasClass('soundNotLoaded')) { sounds.playSound(keyInfo[key]); } else { // User tries to play a not-loaded sound @@ -154,7 +154,7 @@ function setKeyEvents() { // User presses the delete key } else if (key === 'DELETE' || key === 'BACK_SPACE') { id = $('.waveformed-key').attr('id'); - if(id === undefined){ + if (id === undefined) { return; } // If the deleted sound was in the keys @@ -181,29 +181,29 @@ function setKeyEvents() { waveforms.reset(); } else if (key === 'SPACE') { // Play from the playlist - var soundId; - if(playlist.getFirstPlaylistItem() === 'no sounds!'){ - return; - } - // If a sound is playing, make sure to stop it, not play the first one - if(playlistPlayingSoundInfo){ - soundId = playlistPlayingSoundInfo.id; - } else { // Get first playlist sound - soundId = playlist.getFirstPlaylistItem(); - //console.log('got the first item'); - } - sounds.playSound(playlistInfo[soundId]); + var soundId; + if (playlist.getFirstPlaylistItem() === 'no sounds!') { + return; + } + // If a sound is playing, make sure to stop it, not play the first one + if (playlistPlayingSoundInfo) { + soundId = playlistPlayingSoundInfo.id; + } else { // Get first playlist sound + soundId = playlist.getFirstPlaylistItem(); + //console.log('got the first item'); + } + sounds.playSound(playlistInfo[soundId]); } - if(key === 'ESCAPE'){ + if (key === 'ESCAPE') { // Stop all playing sounds immediately - for(key in keyInfo){ - if(keyInfo[key].howl.playing()){ + for (key in keyInfo) { + if (keyInfo[key].howl.playing()) { keyInfo[key].howl.stop(); } } - for(key in playlistInfo){ - if(playlistInfo[key].howl.playing()){ + for (key in playlistInfo) { + if (playlistInfo[key].howl.playing()) { playlistInfo[key].howl.stop(); } } @@ -213,20 +213,20 @@ function setKeyEvents() { } - Mousetrap.bind(['command+x', 'ctrl+x'], function() { - if(!$(e.target).is('input')){ + Mousetrap.bind(['command+x', 'ctrl+x'], function () { + if (!$(e.target).is('input')) { util.cutKey(waveformedInfo); return false; } }); - Mousetrap.bind(['command+c', 'ctrl+c'], function() { - if(!$(e.target).is('input')){ + Mousetrap.bind(['command+c', 'ctrl+c'], function () { + if (!$(e.target).is('input')) { util.copyKey(waveformedInfo); return false; } }); - Mousetrap.bind(['command+v', 'ctrl+v'], function() { - if(!$(e.target).is('input')){ + Mousetrap.bind(['command+v', 'ctrl+v'], function () { + if (!$(e.target).is('input')) { util.pasteKey(waveformedInfo); return false; } @@ -235,18 +235,18 @@ function setKeyEvents() { return false; }); - $(document).keyup(function(e) { + $(document).keyup(function (e) { var key = keyboardMap[e.which]; var code = e.which; - if(key === 'CONTROL'){ + if (key === 'CONTROL') { ctrl = false; $('#waveform').css('pointer-events', 'inherit'); } }); // Close/save sound settings when save key is pressed. - $('#sound-settings-save').click(function(e) { + $('#sound-settings-save').click(function (e) { var tempSoundInfo = settings.saveSoundSettings(); var itIsKeyInfo = (settingsInfoObj === keyInfo); settingsInfoObj[tempSoundInfo.id] = tempSoundInfo; @@ -261,62 +261,62 @@ function setKeyEvents() { $("#color-picker").fadeOut(); }); - $('#sound-settings-fadeInReset').click(function(e) { - settings.resetFade('sound','in'); + $('#sound-settings-fadeInReset').click(function (e) { + settings.resetFade('sound', 'in'); }); - $('#sound-settings-fadeOutReset').click(function(e) { - settings.resetFade('sound','out'); + $('#sound-settings-fadeOutReset').click(function (e) { + settings.resetFade('sound', 'out'); }); - $('#volume-row').dblclick(function(e) { + $('#volume-row').dblclick(function (e) { console.log('yo'); volSlider.noUiSlider.set(100); }); - $('#page-settings-fadeInReset').click(function(e) { - settings.resetFade('page','in'); + $('#page-settings-fadeInReset').click(function (e) { + settings.resetFade('page', 'in'); }); - $('#page-settings-fadeOutReset').click(function(e) { - settings.resetFade('page','out'); + $('#page-settings-fadeOutReset').click(function (e) { + settings.resetFade('page', 'out'); }); // Prevent firing sounds when editing input fields - $('#sound-settings, .input-field').keydown(function(e) { + $('#sound-settings, .input-field').keydown(function (e) { e.stopPropagation(); }); // Open dialog box when browse button is pressed. - $('#browse-button').click(function(e) { + $('#browse-button').click(function (e) { util.openBrowse(); }); - $("#sound-settings-color-container").click(function(e) { + $("#sound-settings-color-container").click(function (e) { view.openColorPicker(); }); - $(".color-picker-color").click(function(e) { + $(".color-picker-color").click(function (e) { colors.setPickedColor(e.target.id); $("#color-picker").fadeOut(); }); // Prevent Dragging files onto main window - $(document).on('drop', function(e) { + $(document).on('drop', function (e) { e.preventDefault(); return false; }); - $(document).on('dragover', function(e) { + $(document).on('dragover', function (e) { e.preventDefault(); return false; }); - $('.search').on('keyup',function(){ + $('.search').on('keyup', function () { var first = true; - $('#playlist-songs li').each(function(){ - if($(this).is(":visible") === true){ - if(first === true){ + $('#playlist-songs li').each(function () { + if ($(this).is(":visible") === true) { + if (first === true) { $(this).css('background-color', 'var(--aM)'); first = false; } else { @@ -329,4 +329,4 @@ function setKeyEvents() { module.exports = { setKeyEvents: setKeyEvents -}; +}; \ No newline at end of file diff --git a/src/scripts/main.js b/src/scripts/main.js index faff3a0..8aa473a 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -40,7 +40,7 @@ var reloadSound = false; /** * Set up program **/ -$(document).ready(function() { +$(document).ready(function () { settingsInfo = storage.getInfoObj("settings"); // Load the program settings storage.checkAgainstDefault(settingsInfo, "settings"); view.buildKeyboard(); // Create all the keys @@ -53,7 +53,7 @@ $(document).ready(function() { // If there is no pagesInfo object, try loading legacy keyInfo into first page pages.ensurePageExists(1); // Update all pages with any new properties - Object.keys(pagesInfo).map(function(page, index) { + Object.keys(pagesInfo).map(function (page, index) { storage.checkAgainstDefault(pagesInfo[page], "pageInfo"); }); pages.registerKeyInfos(); // register all sounds and put them on keys @@ -62,7 +62,7 @@ $(document).ready(function() { playlistInfo = storage.getInfoObj("playlistInfo"); // Load all of the playlist sounds from storage playlist.registerPlaylistItems(); - Object.keys(pagesInfo).map(function(page, index) { + Object.keys(pagesInfo).map(function (page, index) { pagesNumSounds += Object.keys(pagesInfo[page].keyInfo).length; }); update.checkForUpdate(); @@ -79,7 +79,7 @@ $(document).ready(function() { }); $("#settings-modal").modal({ - complete: function() { + complete: function () { console.log("SAVING GENERAL SETTINGS!"); settings.saveSettings(); } @@ -101,14 +101,13 @@ $(document).ready(function() { }); // Set editable text properties $(".editable").editable( - function(value, settings) { + function (value, settings) { if (value === "") { return "Hit enter after typing!"; } else { return value; } - }, - { + }, { type: "text", tooltip: "Click to edit" } @@ -117,7 +116,7 @@ $(document).ready(function() { $(".global-settings-table").hide(); $("#keyboard" + currentPage).show(); - $(document).ready(function() { + $(document).ready(function () { $("select").material_select(); }); @@ -133,10 +132,10 @@ $(document).ready(function() { max: 125 }, format: { - to: function(value) { + to: function (value) { return Math.round(value) + "%"; }, - from: function(value) { + from: function (value) { return value.replace("%", ""); } } @@ -156,7 +155,7 @@ $(document).ready(function() { restriction: "parent" } }) - .on("resizemove", function(event) { + .on("resizemove", function (event) { var target = event.target, x = parseFloat(target.getAttribute("data-x")) || 0; // update the element's style @@ -167,7 +166,7 @@ $(document).ready(function() { target.setAttribute("data-x", x); $(event.target).css("transition", "0s"); }) - .on("resizeend", function(event) { + .on("resizeend", function (event) { waveforms.getRegion(); $(event.target).css("transition", "0.5s"); }); @@ -224,13 +223,13 @@ $(document).ready(function() { }); */ - $(".tabs").mousewheel(function(e, delta) { + $(".tabs").mousewheel(function (e, delta) { this.scrollLeft -= delta * 40; e.preventDefault(); }); //open links externally by default - $(document).on("click", 'a[href^="http"]', function(event) { + $(document).on("click", 'a[href^="http"]', function (event) { event.preventDefault(); shell.openExternal(this.href); }); @@ -240,7 +239,7 @@ function restart() { app.relaunch(); app.quit(); } - +/* window.onerror = function(msg, url, line, col, error) { var extra = !col ? "" : "\ncolumn: " + col; extra += !error ? "" : "\nerror: " + error; @@ -262,3 +261,4 @@ window.onerror = function(msg, url, line, col, error) { // Internet Explorer) will be suppressed. return suppressErrorAlert; }; +*/ \ No newline at end of file diff --git a/src/scripts/pages.js b/src/scripts/pages.js index 1e353e7..53a385d 100644 --- a/src/scripts/pages.js +++ b/src/scripts/pages.js @@ -1,14 +1,13 @@ /* jshint esversion: 6 */ -//const soundInfoManager = require("./soundInfoManager"); var settingsPageNum; -$(document).ready(function(){ - $('ul.tabs').on('click', 'a', function(e){ +$(document).ready(function () { + $('ul.tabs').on('click', 'a', function (e) { switchPage(e); }); - $('ul.tabs').on('contextmenu', 'a', function(e){ + $('ul.tabs').on('contextmenu', 'a', function (e) { var pageId = e.target.id; - if(!pageId){ + if (!pageId) { pageId = $(e.target).closest('a').prop('id'); } settingsPageNum = pageId.substring(pageId.length - 1); @@ -16,15 +15,15 @@ $(document).ready(function(){ settings.openPageSettings(settingsPageNum); }); - $('#page-save-button').on('click', function(e){ + $('#page-save-button').on('click', function (e) { settings.savePageSettings(settingsPageNum); }); - $('body').keydown(function(e){ - if(e.which > 111 && e.which < 120){ + $('body').keydown(function (e) { + if (e.which > 111 && e.which < 120) { e.preventDefault(); var pageNum; - switch(e.which){ + switch (e.which) { case 112: pageNum = 1; break; @@ -55,14 +54,14 @@ $(document).ready(function(){ }); }); -function registerKeyInfos(){ +function registerKeyInfos() { console.log(pagesInfo); - Object.keys(pagesInfo).map(function(page, index){ + Object.keys(pagesInfo).map(function (page, index) { var tempKeyInfo = pagesInfo[page].keyInfo; - Object.keys(tempKeyInfo).map(function(id, index) { + Object.keys(tempKeyInfo).map(function (id, index) { // Ensure all parameters are up to date storage.checkAgainstDefault(tempKeyInfo[id], 'soundInfo'); - if((tempKeyInfo[id].path === '') || (id.charAt(4) != page.charAt(4))){ + if ((tempKeyInfo[id].path === '') || (id.charAt(4) != page.charAt(4))) { delete tempKeyInfo[id]; } else { $("#" + tempKeyInfo[id].id).find('.audioName').text(tempKeyInfo[id].name); @@ -72,8 +71,8 @@ function registerKeyInfos(){ }); } -function ensurePageExists(pageNum){ - if(!pagesInfo.hasOwnProperty('page' + pageNum)){ +function ensurePageExists(pageNum) { + if (!pagesInfo.hasOwnProperty('page' + pageNum)) { pagesInfo['page' + pageNum] = {}; storage.checkAgainstDefault(pagesInfo['page' + pageNum], 'pageInfo'); } @@ -84,7 +83,7 @@ function switchPage(e) { pagesInfo['page' + currentPage].keyInfo = keyInfo; $('#keyboard' + currentPage).hide(); var pageId = e.target.id; - if(!pageId){ + if (!pageId) { pageId = $(e.target).closest('a').prop('id'); } currentPage = pageId.substring(pageId.length - 1); @@ -94,32 +93,32 @@ function switchPage(e) { //$.fn.pagepiling.moveTo(currentPage); } -function loadNames(){ - for(var i = 1; i < 9; i++){ +function loadNames() { + for (var i = 1; i < 9; i++) { try { var name = pagesInfo['page' + i].name; } catch (e) { return; } - if(name){ + if (name) { $('#page' + i + ' span').text(name); } } } -function getFadeTime(pageInfo, direction){ - if(direction === 'in'){ - if(pageInfo.fadeInTime === undefined){ - return settingsInfo.general.fadeInTime/1000; +function getFadeTime(pageInfo, direction) { + if (direction === 'in') { + if (pageInfo.fadeInTime === undefined) { + return settingsInfo.general.fadeInTime / 1000; } else { - return pageInfo.fadeInTime/1000; + return pageInfo.fadeInTime / 1000; } - } else if(direction === 'out'){ - if(pageInfo.fadeOutTime === undefined){ - return settingsInfo.general.fadeOutTime/1000; + } else if (direction === 'out') { + if (pageInfo.fadeOutTime === undefined) { + return settingsInfo.general.fadeOutTime / 1000; } else { - return pageInfo.fadeOutTime/1000; + return pageInfo.fadeOutTime / 1000; } } @@ -131,4 +130,4 @@ module.exports = { switchPage: switchPage, loadNames: loadNames, getFadeTime: getFadeTime -}; +}; \ No newline at end of file diff --git a/src/scripts/soundInfoManager.js b/src/scripts/soundInfoManager.js deleted file mode 100644 index 6a93baf..0000000 --- a/src/scripts/soundInfoManager.js +++ /dev/null @@ -1,50 +0,0 @@ -/*jshint esversion: 6 */ -const sounds = require("./sounds"); -var defaultSoundInfo = sounds.defaultSoundInfo(); - -/** - * @desc: Takes a path and (optional) id and returns a default soundInfo object - * @param: path: the complete path to the sound file - id: (optional) The id for the sound. Defaults to default name - */ -function createNewSoundInfo(path, id) { - // Get template - var newSoundInfo = util.cloneObj(defaultSoundInfo); - //storage.checkAgainstDefault(tempObj, 'soundInfo'); - // Write new specific info - newSoundInfo.name = util.cleanName(path); - newSoundInfo.id = id || util.prepareForId(tempObj.name); - newSoundInfo.path = path; - - // Define which section it is part of - id will be undefined if it's in the playlist - if(id === undefined){ - // New sound is in the playlist - newSoundInfo.infoObj = "playlist"; - // Create a unique id for the playlist item (to allow multiple of same!) - var count = 0; - var duplicate, tempId; - do { - duplicate = false; - tempId = (count > 0) ? newSoundInfo.id + count : newSoundInfo.id; - for (var item in playlistInfo) { - if(playlistInfo[item].id == tempId){ - duplicate = true; - count++; - break; - } - } - } while (duplicate); - newSoundInfo.id = (count > 0) ? tempId : newSoundInfo.id; - - } else { - // New sound is in the pages - newSoundInfo.infoObj = "key"; - } - // Create howl instance - sounds.createNewHowl(newSoundInfo); - return newSoundInfo; -} - -module.exports = { - createNewSoundInfo: createNewSoundInfo, -}; diff --git a/src/scripts/sounds.js b/src/scripts/sounds.js index 04e4f94..9ffe723 100644 --- a/src/scripts/sounds.js +++ b/src/scripts/sounds.js @@ -2,7 +2,7 @@ * These functions deal with anything related to the audio engine */ /*jshint esversion: 6 */ - +const util = require("./util"); var firstPlaylistSound; var loadedCount = 0; var stopping = 0; @@ -20,36 +20,36 @@ function createNewHowl(soundInfo) { src: [soundInfo.path], loop: soundInfo.loop, html5: true, - onend: function() { + onend: function () { stop(soundInfo); }, - onload: function() { + onload: function () { if (soundInfo.endTime === null) { soundInfo.endTime = soundInfo.howl.duration(); } addToLoadingBar(); }, - onloaderror: function(){ + onloaderror: function () { addToLoadingBar(); }, - onplay: function() { + onplay: function () { var fadeTime = getFadeTime(soundInfo, "in"); if (fadeTime >= 0) { soundInfo.fadeIn(); } soundInfo.paused = false; - soundInfo.endCheck = setInterval(function() { + soundInfo.endCheck = setInterval(function () { if (soundInfo.atEnding()) { clearInterval(soundInfo.endCheck); soundInfo.fadeOut(); } }, 50); }, - onpause: function() { + onpause: function () { soundInfo.paused = true; clearInterval(soundInfo.endCheck); }, - onstop: function() { + onstop: function () { soundInfo.paused = false; clearInterval(soundInfo.fadeInterval); clearInterval(soundInfo.endCheck); @@ -67,7 +67,7 @@ function createNewHowl(soundInfo) { } } -function addToLoadingBar(){ +function addToLoadingBar() { loadedCount++; var loadedPercent = loadedCount / totalNumSounds * 100 + "%"; $("#loadedCount").width(loadedPercent); @@ -108,7 +108,7 @@ function playSound(soundInfo) { soundInfo.howl.seek(soundInfo.startTime); } var fadeTime = getFadeTime(soundInfo, "in"); - soundInfo.howl.volume(fadeTime > 0 ? 0 : 1); + soundInfo.howl.volume(fadeTime > 0 ? 0 : soundInfo.volume); // Fade out currently playing sounds if user has selected solo sounds if ( @@ -117,19 +117,19 @@ function playSound(soundInfo) { soundInfo.infoObj !== "playlist" ) { for (key in keyInfo) { - if (keyInfo[key].howl.playing()) { + if (keyInfo[key].howl && keyInfo[key].howl.playing()) { keyInfo[key].fadeOut(); } } } if (settingsInfo.pages.soloSound === "all") { for (key in keyInfo) { - if (keyInfo[key].howl.playing()) { + if (keyInfo[key].howl && keyInfo[key].howl.playing()) { keyInfo[key].fadeOut(); } } for (var item in playlistInfo) { - if (playlistInfo[item].howl.playing()) { + if (playlistInfo[item].howl && playlistInfo[item].howl.playing()) { playlistInfo[item].fadeOut(); } } @@ -185,64 +185,85 @@ function getDuration(soundInfo) { * @desc: The constructor function for a new sound info object. * @param: None. */ -function defaultSoundInfo() { - return { - id: "", - infoObj: "", - name: "", - path: "", - color: "default", - loop: false, - startTime: 0, - endTime: null, - volume: 0.8, - playlistPosition: undefined, - fadeIn: function() { - if (this.atEnding()) { - sounds.stop(this); +function SoundInfo(path, id) { + this.name = util.cleanName(path); + this.id = id || getPlaylistId(this); + this.path = path; + this.howl = undefined; + this.infoObj = (id === undefined ? "playlist" : "key"); + this.color = "default"; + this.loop = false; + this.startTime = 0; + this.endTime = null; + this.volume = 0.8; + this.playlistPosition = undefined; + + this.fadeIn = function () { + if (this.atEnding()) { + sounds.stop(this); + } + var duration = getFadeTime(this, "in"); + this.fadeInterval = setInterval(() => { + var newVol = this.howl.volume() + this.volume * 50 / duration; + if (newVol >= this.volume) { + newVol = this.volume; + this.howl.volume(newVol); + clearInterval(this.fadeInterval); + return; } - var duration = getFadeTime(this, "in"); - this.fadeInterval = setInterval(() => { - var newVol = this.howl.volume() + this.volume * 50 / duration; - if (newVol >= this.volume) { - newVol = this.volume; - this.howl.volume(newVol); - clearInterval(this.fadeInterval); - return; - } + this.howl.volume(newVol); + }, 50); + } + this.fadeOut = function () { + var duration = getFadeTime(this, "out"); + clearInterval(this.fadeInterval); + if (duration === 0) { + sounds.stop(this); + return; + } + if (this.atEnding()) { + duration = (this.endTime - this.howl.seek()) * 1000; + } + this.fadeInterval = setInterval(() => { + var newVol = this.howl.volume() - this.volume * 50 / duration; + if (newVol <= 0) { + newVol = 0; this.howl.volume(newVol); - }, 50); - }, - fadeOut: function() { - var duration = getFadeTime(this, "out"); - clearInterval(this.fadeInterval); - if (duration === 0) { + clearInterval(this.fadeInterval); sounds.stop(this); return; } - if (this.atEnding()) { - duration = (this.endTime - this.howl.seek()) * 1000; - } - this.fadeInterval = setInterval(() => { - var newVol = this.howl.volume() - this.volume * 50 / duration; - if (newVol <= 0) { - newVol = 0; - this.howl.volume(newVol); - clearInterval(this.fadeInterval); - sounds.stop(this); - return; + this.howl.volume(newVol); + }, 50); + } + this.fadeInterval = undefined; + this.fadeInTime = undefined; + this.fadeOutTime = undefined; + this.atEnding = function () { + var fadeT = getFadeTime(this, "out"); + return this.howl.seek() + fadeT / 1000 > this.endTime; + } + + + function getPlaylistId(that) { + // New sound is in the playlist + // Create a unique id for the playlist item (to allow multiple of same!) + var preppedId = util.prepareForId(that.name); + var count = 0, + tempId; + do { + var duplicate = false; + tempId = (count > 0) ? (preppedId + count) : preppedId; + for (var item in playlistInfo) { + if (playlistInfo[item].id == tempId) { + duplicate = true; + count++; + break; } - this.howl.volume(newVol); - }, 50); - }, - fadeInterval: undefined, - fadeInTime: undefined, - fadeOutTime: undefined, - atEnding: function() { - var fadeT = getFadeTime(this, "out"); - return this.howl.seek() + fadeT / 1000 > this.endTime; - } - }; + } + } while (duplicate); + return tempId; + } } function getFadeTime(soundInfo, direction) { @@ -287,6 +308,6 @@ module.exports = { playSound: playSound, getDuration: getDuration, stop: stop, - defaultSoundInfo: defaultSoundInfo, - getFadeTime: getFadeTime -}; + getFadeTime: getFadeTime, + SoundInfo: SoundInfo +}; \ No newline at end of file diff --git a/src/scripts/storage.js b/src/scripts/storage.js index e526cc6..d0c22da 100644 --- a/src/scripts/storage.js +++ b/src/scripts/storage.js @@ -3,39 +3,38 @@ */ /*jshint esversion: 6 */ -const soundInfoManager = require("./soundInfoManager"); const jetpack = require('fs-jetpack'); const app = require('electron').remote.app; var appDir = app.getPath('userData'); -$(function(){ +$(function () { jetpack.dir(appDir).dir('data'); }); var dataDir = appDir + '\\data\\'; var defaults = { - 'soundInfo': sounds.defaultSoundInfo(), - 'settings' : { - 'general':{ + 'soundInfo': new sounds.SoundInfo(""), + 'settings': { + 'general': { 'prereleaseUpdates': false, 'stopSounds': false, 'markPlayed': true, 'fadeInTime': 1500, 'fadeOutTime': 1500 }, - 'playlist':{ + 'playlist': { "soundToBottomAfterPlay": true, "soundDeleteAfterPlay": false, order: '' }, - 'pages':{ + 'pages': { "soloSound": "off" }, - 'utility':{ + 'utility': { "pVersion": "0.1.0" } }, - 'pageInfo' : { + 'pageInfo': { 'name': '', fadeInTime: undefined, fadeOutTime: undefined, @@ -55,7 +54,7 @@ function getInfoObj(objName) { var tempObj = {}; var objPath = dataDir + objName + '.json'; // Use JSON storage if it exists, otherwise pull from (legacy) localStorage - if(jetpack.exists(objPath)){ + if (jetpack.exists(objPath)) { tempObj = JSON.parse(jetpack.read(objPath)); } else { console.log(objName + '.json does not exist. Using localStorage instead.'); @@ -79,10 +78,10 @@ function storeObj(objName, obj) { // Clean soundInstances from storage var clonedObj = util.cloneObj(obj); //console.log(clonedObj); - if(objName === 'playlistInfo'){ + if (objName === 'playlistInfo') { stripPlayState(clonedObj); - } else if(objName === 'pagesInfo'){ - Object.keys(clonedObj).map(function(prop, index) { + } else if (objName === 'pagesInfo') { + Object.keys(clonedObj).map(function (prop, index) { stripPlayState(clonedObj[prop].keyInfo); }); } @@ -93,11 +92,11 @@ function storeObj(objName, obj) { console.log('Storing new ' + objName + ' to json.'); } -function deleteObj(objName){ +function deleteObj(objName) { jetpack.remove(dataDir + objName + '.json'); } -function emptyObj(objName, obj){ +function emptyObj(objName, obj) { obj = {}; storeObj(objName, obj); } @@ -110,12 +109,12 @@ function emptyObj(objName, obj){ function checkAgainstDefault(obj, defaultName) { var changed = false; // Update the object with any new properties - Object.keys(defaults[defaultName]).map(function(prop, index) { + Object.keys(defaults[defaultName]).map(function (prop, index) { if (!obj.hasOwnProperty(prop)) { obj[prop] = defaults[defaultName][prop]; } - if(typeof defaults[defaultName][prop] == "object" && defaults[defaultName][prop] !== null && defaultName === 'settings'){ - Object.keys(defaults[defaultName][prop]).map(function(prop2, index) { + if (typeof defaults[defaultName][prop] == "object" && defaults[defaultName][prop] !== null && defaultName === 'settings') { + Object.keys(defaults[defaultName][prop]).map(function (prop2, index) { if (!obj[prop].hasOwnProperty(prop2)) { obj[prop][prop2] = defaults[defaultName][prop][prop2]; } @@ -124,12 +123,12 @@ function checkAgainstDefault(obj, defaultName) { }); // Check that the object does not have depreciated properties (and delete them) - Object.keys(obj).map(function(prop, index) { + Object.keys(obj).map(function (prop, index) { if (!defaults[defaultName].hasOwnProperty(prop)) { delete obj[prop]; } - if(typeof obj[prop] == "object" && obj[prop] !== null && defaultName === 'settings'){ - Object.keys(obj[prop]).map(function(prop2, index) { + if (typeof obj[prop] == "object" && obj[prop] !== null && defaultName === 'settings') { + Object.keys(obj[prop]).map(function (prop2, index) { if (!defaults[defaultName][prop].hasOwnProperty(prop2)) { delete obj[prop][prop2]; } @@ -138,8 +137,8 @@ function checkAgainstDefault(obj, defaultName) { }); } -function stripPlayState(infoObj){ - Object.keys(infoObj).map(function(prop, index) { +function stripPlayState(infoObj) { + Object.keys(infoObj).map(function (prop, index) { delete infoObj[prop].howl; }); } @@ -150,4 +149,4 @@ module.exports = { deleteObj: deleteObj, checkAgainstDefault: checkAgainstDefault, emptyObj: emptyObj -}; +}; \ No newline at end of file diff --git a/src/scripts/vendor/howler v2.0.3.min.js b/src/scripts/vendor/howler v2.0.3.min.js deleted file mode 100644 index 0dee831..0000000 --- a/src/scripts/vendor/howler v2.0.3.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! howler.js v2.0.3 | (c) 2013-2017, James Simpson of GoldFire Studios | MIT License | howlerjs.com */ -!function(){"use strict";var e=function(){this.init()};e.prototype={init:function(){var e=this||n;return e._counter=0,e._codecs={},e._howls=[],e._muted=!1,e._volume=1,e._canPlayEvent="canplaythrough",e._navigator="undefined"!=typeof window&&window.navigator?window.navigator:null,e.masterGain=null,e.noAudio=!1,e.usingWebAudio=!0,e.autoSuspend=!0,e.ctx=null,e.mobileAutoEnable=!0,e._setup(),e},volume:function(e){var o=this||n;if(e=parseFloat(e),o.ctx||_(),void 0!==e&&e>=0&&e<=1){if(o._volume=e,o._muted)return o;o.usingWebAudio&&(o.masterGain.gain.value=e);for(var t=0;t=0;o--)e._howls[o].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,_()),e},codecs:function(e){return(this||n)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||n;if(e.state=e.ctx?e.ctx.state||"running":"running",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{var o=new Audio;void 0===o.oncanplaythrough&&(e._canPlayEvent="canplay")}catch(n){e.noAudio=!0}else e.noAudio=!0;try{var o=new Audio;o.muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||n,o=null;try{o="undefined"!=typeof Audio?new Audio:null}catch(n){return e}if(!o||"function"!=typeof o.canPlayType)return e;var t=o.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator&&e._navigator.userAgent.match(/OPR\/([0-6].)/g),a=r&&parseInt(r[0].split("/")[1],10)<33;return e._codecs={mp3:!(a||!t&&!o.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!t,opus:!!o.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!o.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),aac:!!o.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!o.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(o.canPlayType("audio/x-m4a;")||o.canPlayType("audio/m4a;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(o.canPlayType("audio/x-mp4;")||o.canPlayType("audio/mp4;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),webm:!!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),dolby:!!o.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(o.canPlayType("audio/x-flac;")||o.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_enableMobileAudio:function(){var e=this||n,o=/iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk|Mobi/i.test(e._navigator&&e._navigator.userAgent),t=!!("ontouchend"in window||e._navigator&&e._navigator.maxTouchPoints>0||e._navigator&&e._navigator.msMaxTouchPoints>0);if(!e._mobileEnabled&&e.ctx&&(o||t)){e._mobileEnabled=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var r=function(){var n=e.ctx.createBufferSource();n.buffer=e._scratchBuffer,n.connect(e.ctx.destination),void 0===n.start?n.noteOn(0):n.start(0),n.onended=function(){n.disconnect(0),e._mobileEnabled=!0,e.mobileAutoEnable=!1,document.removeEventListener("touchend",r,!0)}};return document.addEventListener("touchend",r,!0),e}},_autoSuspend:function(){var e=this;if(e.autoSuspend&&e.ctx&&void 0!==e.ctx.suspend&&n.usingWebAudio){for(var o=0;o0?i._seek:t._sprite[e][0]/1e3),_=Math.max(0,(t._sprite[e][0]+t._sprite[e][1])/1e3-d),s=1e3*_/Math.abs(i._rate);i._paused=!1,i._ended=!1,i._sprite=e,i._seek=d,i._start=t._sprite[e][0]/1e3,i._stop=(t._sprite[e][0]+t._sprite[e][1])/1e3,i._loop=!(!i._loop&&!t._sprite[e][2]);var l=i._node;if(t._webAudio){var c=function(){t._refreshBuffer(i);var e=i._muted||t._muted?0:i._volume;l.gain.setValueAtTime(e,n.ctx.currentTime),i._playStart=n.ctx.currentTime,void 0===l.bufferSource.start?i._loop?l.bufferSource.noteGrainOn(0,d,86400):l.bufferSource.noteGrainOn(0,d,_):i._loop?l.bufferSource.start(0,d,86400):l.bufferSource.start(0,d,_),s!==1/0&&(t._endTimers[i._id]=setTimeout(t._ended.bind(t,i),s)),o||setTimeout(function(){t._emit("play",i._id)},0)},f="running"===n.state;if("loaded"===t._state&&f)c();else{var p=f||"loaded"!==t._state?"load":"resume";t.once(p,c,f?i._id:null),t._clearTimer(i._id)}}else{var v=function(){l.currentTime=d,l.muted=i._muted||t._muted||n._muted||l.muted,l.volume=i._volume*n.volume(),l.playbackRate=i._rate,l.play(),s!==1/0&&(t._endTimers[i._id]=setTimeout(t._ended.bind(t,i),s)),o||t._emit("play",i._id)},m="loaded"===t._state&&(window&&window.ejecta||!l.readyState&&n._navigator.isCocoonJS);if(4===l.readyState||m)v();else{var h=function(){v(),l.removeEventListener(n._canPlayEvent,h,!1)};l.addEventListener(n._canPlayEvent,h,!1),t._clearTimer(i._id)}}return i._id},pause:function(e){var n=this;if("loaded"!==n._state)return n._queue.push({event:"pause",action:function(){n.pause(e)}}),n;for(var o=n._getSoundIds(e),t=0;t=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else r.length>=2&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var a;if(!(void 0!==e&&e>=0&&e<=1))return a=o?t._soundById(o):t._sounds[0],a?a._volume:0;if("loaded"!==t._state)return t._queue.push({event:"volume",action:function(){t.volume.apply(t,r)}}),t;void 0===o&&(t._volume=e),o=t._getSoundIds(o);for(var u=0;uo?"out":"in",d=u/.01,_=d>0?t/d:t;if(_<4&&(d=Math.ceil(d/(4/_)),_=4),"loaded"!==a._state)return a._queue.push({event:"fade",action:function(){a.fade(e,o,t,r)}}),a;a.volume(e,r);for(var s=a._getSoundIds(r),l=0;l0&&(v+="in"===i?.01:-.01),v=Math.max(0,v),v=Math.min(1,v),v=Math.round(100*v)/100,a._webAudio?(void 0===r&&(a._volume=v),t._volume=v):a.volume(v,n,!0),(oe&&v>=o)&&(clearInterval(t._interval),t._interval=null,a.volume(o,n),a._emit("fade",n))}.bind(a,s[l],c),_)}}return a},_stopFade:function(e){var o=this,t=o._soundById(e);return t&&t._interval&&(o._webAudio&&t._node.gain.cancelScheduledValues(n.ctx.currentTime),clearInterval(t._interval),t._interval=null,o._emit("fade",e)),o},loop:function(){var e,n,o,t=this,r=arguments;if(0===r.length)return t._loop;if(1===r.length){if("boolean"!=typeof r[0])return!!(o=t._soundById(parseInt(r[0],10)))&&o._loop;e=r[0],t._loop=e}else 2===r.length&&(e=r[0],n=parseInt(r[1],10));for(var a=t._getSoundIds(n),u=0;u=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var i;if("number"!=typeof e)return i=t._soundById(o),i?i._rate:t._rate;if("loaded"!==t._state)return t._queue.push({event:"rate",action:function(){t.rate.apply(t,r)}}),t;void 0===o&&(t._rate=e),o=t._getSoundIds(o);for(var d=0;d=0?o=parseInt(r[0],10):(o=t._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));if(void 0===o)return t;if("loaded"!==t._state)return t._queue.push({event:"seek",action:function(){t.seek.apply(t,r)}}),t;var i=t._soundById(o);if(i){if(!("number"==typeof e&&e>=0)){if(t._webAudio){var d=t.playing(o)?n.ctx.currentTime-i._playStart:0,_=i._rateSeek?i._rateSeek-i._seek:0;return i._seek+(_+d*Math.abs(i._rate))}return i._node.currentTime}var s=t.playing(o);s&&t.pause(o,!0),i._seek=e,i._ended=!1,t._clearTimer(o),s&&t.play(o,!0),!t._webAudio&&i._node&&(i._node.currentTime=e),t._emit("seek",o)}return t},playing:function(e){var n=this;if("number"==typeof e){var o=n._soundById(e);return!!o&&!o._paused}for(var t=0;t=0&&n._howls.splice(a,1)}var u=!0;for(t=0;t=0;a--)r[a].id&&r[a].id!==n&&"load"!==e||(setTimeout(function(e){e.call(this,n,o)}.bind(t,r[a].fn),0),r[a].once&&t.off(e,r[a].fn,r[a].id));return t},_loadQueue:function(){var e=this;if(e._queue.length>0){var n=e._queue[0];e.once(n.event,function(){e._queue.shift(),e._loadQueue()}),n.action()}return e},_ended:function(e){var o=this,t=e._sprite,r=!(!e._loop&&!o._sprite[t][2]);if(o._emit("end",e._id),!o._webAudio&&r&&o.stop(e._id,!0).play(e._id),o._webAudio&&r){o._emit("play",e._id),e._seek=e._start||0,e._rateSeek=0,e._playStart=n.ctx.currentTime;var a=1e3*(e._stop-e._start)/Math.abs(e._rate);o._endTimers[e._id]=setTimeout(o._ended.bind(o,e),a)}return o._webAudio&&!r&&(e._paused=!0,e._ended=!0,e._seek=e._start||0,e._rateSeek=0,o._clearTimer(e._id),o._cleanBuffer(e._node),n._autoSuspend()),o._webAudio||r||o.stop(e._id),o},_clearTimer:function(e){var n=this;return n._endTimers[e]&&(clearTimeout(n._endTimers[e]),delete n._endTimers[e]),n},_soundById:function(e){for(var n=this,o=0;o=0;t--){if(o<=n)return;e._sounds[t]._ended&&(e._webAudio&&e._sounds[t]._node&&e._sounds[t]._node.disconnect(0),e._sounds.splice(t,1),o--)}}},_getSoundIds:function(e){var n=this;if(void 0===e){for(var o=[],t=0;t0&&(r[o._src]=e,d(o,e))},function(){o._emit("loaderror",null,"Decoding audio data failed.")})},d=function(e,n){n&&!e._duration&&(e._duration=n.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},_=function(){try{"undefined"!=typeof AudioContext?n.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?n.ctx=new webkitAudioContext:n.usingWebAudio=!1}catch(e){n.usingWebAudio=!1}var e=/iP(hone|od|ad)/.test(n._navigator&&n._navigator.platform),o=n._navigator&&n._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),t=o?parseInt(o[1],10):null;if(e&&t&&t<9){var r=/safari/.test(n._navigator&&n._navigator.userAgent.toLowerCase());(n._navigator&&n._navigator.standalone&&!r||n._navigator&&!n._navigator.standalone&&!r)&&(n.usingWebAudio=!1)}n.usingWebAudio&&(n.masterGain=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),n.masterGain.gain.value=1,n.masterGain.connect(n.ctx.destination)),n._setup()};"function"==typeof define&&define.amd&&define([],function(){return{Howler:n,Howl:o}}),"undefined"!=typeof exports&&(exports.Howler=n,exports.Howl=o),"undefined"!=typeof window?(window.HowlerGlobal=e,window.Howler=n,window.Howl=o,window.Sound=t):"undefined"!=typeof global&&(global.HowlerGlobal=e,global.Howler=n,global.Howl=o,global.Sound=t)}(); -/*! Spatial Plugin */ -!function(){"use strict";HowlerGlobal.prototype._pos=[0,0,0],HowlerGlobal.prototype._orientation=[0,0,-1,0,1,0],HowlerGlobal.prototype.stereo=function(n){var e=this;if(!e.ctx||!e.ctx.listener)return e;for(var o=e._howls.length-1;o>=0;o--)e._howls[o].stereo(n);return e},HowlerGlobal.prototype.pos=function(n,e,o){var t=this;return t.ctx&&t.ctx.listener?(e="number"!=typeof e?t._pos[1]:e,o="number"!=typeof o?t._pos[2]:o,"number"!=typeof n?t._pos:(t._pos=[n,e,o],t.ctx.listener.setPosition(t._pos[0],t._pos[1],t._pos[2]),t)):t},HowlerGlobal.prototype.orientation=function(n,e,o,t,r,i){var a=this;if(!a.ctx||!a.ctx.listener)return a;var p=a._orientation;return e="number"!=typeof e?p[1]:e,o="number"!=typeof o?p[2]:o,t="number"!=typeof t?p[3]:t,r="number"!=typeof r?p[4]:r,i="number"!=typeof i?p[5]:i,"number"!=typeof n?p:(a._orientation=[n,e,o,t,r,i],a.ctx.listener.setOrientation(n,e,o,t,r,i),a)},Howl.prototype.init=function(n){return function(e){var o=this;return o._orientation=e.orientation||[1,0,0],o._stereo=e.stereo||null,o._pos=e.pos||null,o._pannerAttr={coneInnerAngle:void 0!==e.coneInnerAngle?e.coneInnerAngle:360,coneOuterAngle:void 0!==e.coneOuterAngle?e.coneOuterAngle:360,coneOuterGain:void 0!==e.coneOuterGain?e.coneOuterGain:0,distanceModel:void 0!==e.distanceModel?e.distanceModel:"inverse",maxDistance:void 0!==e.maxDistance?e.maxDistance:1e4,panningModel:void 0!==e.panningModel?e.panningModel:"HRTF",refDistance:void 0!==e.refDistance?e.refDistance:1,rolloffFactor:void 0!==e.rolloffFactor?e.rolloffFactor:1},o._onstereo=e.onstereo?[{fn:e.onstereo}]:[],o._onpos=e.onpos?[{fn:e.onpos}]:[],o._onorientation=e.onorientation?[{fn:e.onorientation}]:[],n.call(this,e)}}(Howl.prototype.init),Howl.prototype.stereo=function(e,o){var t=this;if(!t._webAudio)return t;if("loaded"!==t._state)return t._queue.push({event:"stereo",action:function(){t.stereo(e,o)}}),t;var r=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===o){if("number"!=typeof e)return t._stereo;t._stereo=e,t._pos=[e,0,0]}for(var i=t._getSoundIds(o),a=0;a= 0 && vol <= 1) { - self._volume = vol; - - // Don't update any of the nodes if we are muted. - if (self._muted) { - return self; - } - - // When using Web Audio, we just need to adjust the master gain. - if (self.usingWebAudio) { - self.masterGain.gain.value = vol; - } - - // Loop through and change volume for all HTML5 audio nodes. - for (var i=0; i=0; i--) { - self._howls[i].unload(); - } - - // Create a new AudioContext to make sure it is fully reset. - if (self.usingWebAudio && self.ctx && typeof self.ctx.close !== 'undefined') { - self.ctx.close(); - self.ctx = null; - setupAudioContext(); - } - - return self; - }, - - /** - * Check for codec support of specific extension. - * @param {String} ext Audio file extention. - * @return {Boolean} - */ - codecs: function(ext) { - return (this || Howler)._codecs[ext.replace(/^x-/, '')]; - }, - - /** - * Setup various state values for global tracking. - * @return {Howler} - */ - _setup: function() { - var self = this || Howler; - - // Keeps track of the suspend/resume state of the AudioContext. - self.state = self.ctx ? self.ctx.state || 'running' : 'running'; - - // Automatically begin the 30-second suspend process - self._autoSuspend(); - - // Check if audio is available. - if (!self.usingWebAudio) { - // No audio is available on this system if noAudio is set to true. - if (typeof Audio !== 'undefined') { - try { - var test = new Audio(); - - // Check if the canplaythrough event is available. - if (typeof test.oncanplaythrough === 'undefined') { - self._canPlayEvent = 'canplay'; - } - } catch(e) { - self.noAudio = true; - } - } else { - self.noAudio = true; - } - } - - // Test to make sure audio isn't disabled in Internet Explorer. - try { - var test = new Audio(); - if (test.muted) { - self.noAudio = true; - } - } catch (e) {} - - // Check for supported codecs. - if (!self.noAudio) { - self._setupCodecs(); - } - - return self; - }, - - /** - * Check for browser support for various codecs and cache the results. - * @return {Howler} - */ - _setupCodecs: function() { - var self = this || Howler; - var audioTest = null; - - // Must wrap in a try/catch because IE11 in server mode throws an error. - try { - audioTest = (typeof Audio !== 'undefined') ? new Audio() : null; - } catch (err) { - return self; - } - - if (!audioTest || typeof audioTest.canPlayType !== 'function') { - return self; - } - - var mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, ''); - - // Opera version <33 has mixed MP3 support, so we need to check for and block it. - var checkOpera = self._navigator && self._navigator.userAgent.match(/OPR\/([0-6].)/g); - var isOldOpera = (checkOpera && parseInt(checkOpera[0].split('/')[1], 10) < 33); - - self._codecs = { - mp3: !!(!isOldOpera && (mpegTest || audioTest.canPlayType('audio/mp3;').replace(/^no$/, ''))), - mpeg: !!mpegTest, - opus: !!audioTest.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, ''), - ogg: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''), - oga: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''), - wav: !!audioTest.canPlayType('audio/wav; codecs="1"').replace(/^no$/, ''), - aac: !!audioTest.canPlayType('audio/aac;').replace(/^no$/, ''), - caf: !!audioTest.canPlayType('audio/x-caf;').replace(/^no$/, ''), - m4a: !!(audioTest.canPlayType('audio/x-m4a;') || audioTest.canPlayType('audio/m4a;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), - mp4: !!(audioTest.canPlayType('audio/x-mp4;') || audioTest.canPlayType('audio/mp4;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), - weba: !!audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, ''), - webm: !!audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, ''), - dolby: !!audioTest.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/, ''), - flac: !!(audioTest.canPlayType('audio/x-flac;') || audioTest.canPlayType('audio/flac;')).replace(/^no$/, '') - }; - - return self; - }, - - /** - * Mobile browsers will only allow audio to be played after a user interaction. - * Attempt to automatically unlock audio on the first user interaction. - * Concept from: http://paulbakaus.com/tutorials/html5/web-audio-on-ios/ - * @return {Howler} - */ - _enableMobileAudio: function() { - var self = this || Howler; - - // Only run this on mobile devices if audio isn't already eanbled. - var isMobile = /iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk|Mobi/i.test(self._navigator && self._navigator.userAgent); - var isTouch = !!(('ontouchend' in window) || (self._navigator && self._navigator.maxTouchPoints > 0) || (self._navigator && self._navigator.msMaxTouchPoints > 0)); - if (self._mobileEnabled || !self.ctx || (!isMobile && !isTouch)) { - return; - } - - self._mobileEnabled = false; - - // Some mobile devices/platforms have distortion issues when opening/closing tabs and/or web views. - // Bugs in the browser (especially Mobile Safari) can cause the sampleRate to change from 44100 to 48000. - // By calling Howler.unload(), we create a new AudioContext with the correct sampleRate. - if (!self._mobileUnloaded && self.ctx.sampleRate !== 44100) { - self._mobileUnloaded = true; - self.unload(); - } - - // Scratch buffer for enabling iOS to dispose of web audio buffers correctly, as per: - // http://stackoverflow.com/questions/24119684 - self._scratchBuffer = self.ctx.createBuffer(1, 1, 22050); - - // Call this method on touch start to create and play a buffer, - // then check if the audio actually played to determine if - // audio has now been unlocked on iOS, Android, etc. - var unlock = function() { - // Create an empty buffer. - var source = self.ctx.createBufferSource(); - source.buffer = self._scratchBuffer; - source.connect(self.ctx.destination); - - // Play the empty buffer. - if (typeof source.start === 'undefined') { - source.noteOn(0); - } else { - source.start(0); - } - - // Setup a timeout to check that we are unlocked on the next event loop. - source.onended = function() { - source.disconnect(0); - - // Update the unlocked state and prevent this check from happening again. - self._mobileEnabled = true; - self.mobileAutoEnable = false; - - // Remove the touch start listener. - document.removeEventListener('touchend', unlock, true); - }; - }; - - // Setup a touch start listener to attempt an unlock in. - document.addEventListener('touchend', unlock, true); - - return self; - }, - - /** - * Automatically suspend the Web Audio AudioContext after no sound has played for 30 seconds. - * This saves processing/energy and fixes various browser-specific bugs with audio getting stuck. - * @return {Howler} - */ - _autoSuspend: function() { - var self = this; - - if (!self.autoSuspend || !self.ctx || typeof self.ctx.suspend === 'undefined' || !Howler.usingWebAudio) { - return; - } - - // Check if any sounds are playing. - for (var i=0; i 0 ? sound._seek : self._sprite[sprite][0] / 1000); - var duration = Math.max(0, ((self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000) - seek); - var timeout = (duration * 1000) / Math.abs(sound._rate); - - // Update the parameters of the sound - sound._paused = false; - sound._ended = false; - sound._sprite = sprite; - sound._seek = seek; - sound._start = self._sprite[sprite][0] / 1000; - sound._stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000; - sound._loop = !!(sound._loop || self._sprite[sprite][2]); - - // Begin the actual playback. - var node = sound._node; - if (self._webAudio) { - // Fire this when the sound is ready to play to begin Web Audio playback. - var playWebAudio = function() { - self._refreshBuffer(sound); - - // Setup the playback params. - var vol = (sound._muted || self._muted) ? 0 : sound._volume; - node.gain.setValueAtTime(vol, Howler.ctx.currentTime); - sound._playStart = Howler.ctx.currentTime; - - // Play the sound using the supported method. - if (typeof node.bufferSource.start === 'undefined') { - sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration); - } else { - sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration); - } - - // Start a new timer if none is present. - if (timeout !== Infinity) { - self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); - } - - if (!internal) { - setTimeout(function() { - self._emit('play', sound._id); - }, 0); - } - }; - - var isRunning = (Howler.state === 'running'); - if (self._state === 'loaded' && isRunning) { - playWebAudio(); - } else { - // Wait for the audio to load and then begin playback. - var event = !isRunning && self._state === 'loaded' ? 'resume' : 'load'; - self.once(event, playWebAudio, isRunning ? sound._id : null); - - // Cancel the end timer. - self._clearTimer(sound._id); - } - } else { - // Fire this when the sound is ready to play to begin HTML5 Audio playback. - var playHtml5 = function() { - node.currentTime = seek; - node.muted = sound._muted || self._muted || Howler._muted || node.muted; - node.volume = sound._volume * Howler.volume(); - node.playbackRate = sound._rate; - node.play(); - - // Setup the new end timer. - if (timeout !== Infinity) { - self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); - } - - if (!internal) { - self._emit('play', sound._id); - } - }; - - // Play immediately if ready, or wait for the 'canplaythrough'e vent. - var loadedNoReadyState = (self._state === 'loaded' && (window && window.ejecta || !node.readyState && Howler._navigator.isCocoonJS)); - if (node.readyState === 4 || loadedNoReadyState) { - playHtml5(); - } else { - var listener = function() { - // Begin playback. - playHtml5(); - - // Clear this listener. - node.removeEventListener(Howler._canPlayEvent, listener, false); - }; - node.addEventListener(Howler._canPlayEvent, listener, false); - - // Cancel the end timer. - self._clearTimer(sound._id); - } - } - - return sound._id; - }, - - /** - * Pause playback and save current position. - * @param {Number} id The sound ID (empty to pause all in group). - * @return {Howl} - */ - pause: function(id) { - var self = this; - - // If the sound hasn't loaded, add it to the load queue to pause when capable. - if (self._state !== 'loaded') { - self._queue.push({ - event: 'pause', - action: function() { - self.pause(id); - } - }); - - return self; - } - - // If no id is passed, get all ID's to be paused. - var ids = self._getSoundIds(id); - - for (var i=0; i Returns the group's volume value. - * volume(id) -> Returns the sound id's current volume. - * volume(vol) -> Sets the volume of all sounds in this Howl group. - * volume(vol, id) -> Sets the volume of passed sound id. - * @return {Howl/Number} Returns self or current volume. - */ - volume: function() { - var self = this; - var args = arguments; - var vol, id; - - // Determine the values based on arguments. - if (args.length === 0) { - // Return the value of the groups' volume. - return self._volume; - } else if (args.length === 1 || args.length === 2 && typeof args[1] === 'undefined') { - // First check if this is an ID, and if not, assume it is a new volume. - var ids = self._getSoundIds(); - var index = ids.indexOf(args[0]); - if (index >= 0) { - id = parseInt(args[0], 10); - } else { - vol = parseFloat(args[0]); - } - } else if (args.length >= 2) { - vol = parseFloat(args[0]); - id = parseInt(args[1], 10); - } - - // Update the volume or return the current volume. - var sound; - if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { - // If the sound hasn't loaded, add it to the load queue to change volume when capable. - if (self._state !== 'loaded') { - self._queue.push({ - event: 'volume', - action: function() { - self.volume.apply(self, args); - } - }); - - return self; - } - - // Set the group volume. - if (typeof id === 'undefined') { - self._volume = vol; - } - - // Update one or all volumes. - id = self._getSoundIds(id); - for (var i=0; i to ? 'out' : 'in'; - var steps = diff / 0.01; - var stepLen = (steps > 0) ? len / steps : len; - - // Since browsers clamp timeouts to 4ms, we need to clamp our steps to that too. - if (stepLen < 4) { - steps = Math.ceil(steps / (4 / stepLen)); - stepLen = 4; - } - - // If the sound hasn't loaded, add it to the load queue to fade when capable. - if (self._state !== 'loaded') { - self._queue.push({ - event: 'fade', - action: function() { - self.fade(from, to, len, id); - } - }); - - return self; - } - - // Set the volume to the start position. - self.volume(from, id); - - // Fade the volume of one or all sounds. - var ids = self._getSoundIds(id); - for (var i=0; i 0) { - vol += (dir === 'in' ? 0.01 : -0.01); - } - - // Make sure the volume is in the right bounds. - vol = Math.max(0, vol); - vol = Math.min(1, vol); - - // Round to within 2 decimal points. - vol = Math.round(vol * 100) / 100; - - // Change the volume. - if (self._webAudio) { - if (typeof id === 'undefined') { - self._volume = vol; - } - - sound._volume = vol; - } else { - self.volume(vol, soundId, true); - } - - // When the fade is complete, stop it and fire event. - if ((to < from && vol <= to) || (to > from && vol >= to)) { - clearInterval(sound._interval); - sound._interval = null; - self.volume(to, soundId); - self._emit('fade', soundId); - } - }.bind(self, ids[i], sound), stepLen); - } - } - - return self; - }, - - /** - * Internal method that stops the currently playing fade when - * a new fade starts, volume is changed or the sound is stopped. - * @param {Number} id The sound id. - * @return {Howl} - */ - _stopFade: function(id) { - var self = this; - var sound = self._soundById(id); - - if (sound && sound._interval) { - if (self._webAudio) { - sound._node.gain.cancelScheduledValues(Howler.ctx.currentTime); - } - - clearInterval(sound._interval); - sound._interval = null; - self._emit('fade', id); - } - - return self; - }, - - /** - * Get/set the loop parameter on a sound. This method can optionally take 0, 1 or 2 arguments. - * loop() -> Returns the group's loop value. - * loop(id) -> Returns the sound id's loop value. - * loop(loop) -> Sets the loop value for all sounds in this Howl group. - * loop(loop, id) -> Sets the loop value of passed sound id. - * @return {Howl/Boolean} Returns self or current loop value. - */ - loop: function() { - var self = this; - var args = arguments; - var loop, id, sound; - - // Determine the values for loop and id. - if (args.length === 0) { - // Return the grou's loop value. - return self._loop; - } else if (args.length === 1) { - if (typeof args[0] === 'boolean') { - loop = args[0]; - self._loop = loop; - } else { - // Return this sound's loop value. - sound = self._soundById(parseInt(args[0], 10)); - return sound ? sound._loop : false; - } - } else if (args.length === 2) { - loop = args[0]; - id = parseInt(args[1], 10); - } - - // If no id is passed, get all ID's to be looped. - var ids = self._getSoundIds(id); - for (var i=0; i Returns the first sound node's current playback rate. - * rate(id) -> Returns the sound id's current playback rate. - * rate(rate) -> Sets the playback rate of all sounds in this Howl group. - * rate(rate, id) -> Sets the playback rate of passed sound id. - * @return {Howl/Number} Returns self or the current playback rate. - */ - rate: function() { - var self = this; - var args = arguments; - var rate, id; - - // Determine the values based on arguments. - if (args.length === 0) { - // We will simply return the current rate of the first node. - id = self._sounds[0]._id; - } else if (args.length === 1) { - // First check if this is an ID, and if not, assume it is a new rate value. - var ids = self._getSoundIds(); - var index = ids.indexOf(args[0]); - if (index >= 0) { - id = parseInt(args[0], 10); - } else { - rate = parseFloat(args[0]); - } - } else if (args.length === 2) { - rate = parseFloat(args[0]); - id = parseInt(args[1], 10); - } - - // Update the playback rate or return the current value. - var sound; - if (typeof rate === 'number') { - // If the sound hasn't loaded, add it to the load queue to change playback rate when capable. - if (self._state !== 'loaded') { - self._queue.push({ - event: 'rate', - action: function() { - self.rate.apply(self, args); - } - }); - - return self; - } - - // Set the group rate. - if (typeof id === 'undefined') { - self._rate = rate; - } - - // Update one or all volumes. - id = self._getSoundIds(id); - for (var i=0; i Returns the first sound node's current seek position. - * seek(id) -> Returns the sound id's current seek position. - * seek(seek) -> Sets the seek position of the first sound node. - * seek(seek, id) -> Sets the seek position of passed sound id. - * @return {Howl/Number} Returns self or the current seek position. - */ - seek: function() { - var self = this; - var args = arguments; - var seek, id; - - // Determine the values based on arguments. - if (args.length === 0) { - // We will simply return the current position of the first node. - id = self._sounds[0]._id; - } else if (args.length === 1) { - // First check if this is an ID, and if not, assume it is a new seek position. - var ids = self._getSoundIds(); - var index = ids.indexOf(args[0]); - if (index >= 0) { - id = parseInt(args[0], 10); - } else { - id = self._sounds[0]._id; - seek = parseFloat(args[0]); - } - } else if (args.length === 2) { - seek = parseFloat(args[0]); - id = parseInt(args[1], 10); - } - - // If there is no ID, bail out. - if (typeof id === 'undefined') { - return self; - } - - // If the sound hasn't loaded, add it to the load queue to seek when capable. - if (self._state !== 'loaded') { - self._queue.push({ - event: 'seek', - action: function() { - self.seek.apply(self, args); - } - }); - - return self; - } - - // Get the sound. - var sound = self._soundById(id); - - if (sound) { - if (typeof seek === 'number' && seek >= 0) { - // Pause the sound and update position for restarting playback. - var playing = self.playing(id); - if (playing) { - self.pause(id, true); - } - - // Move the position of the track and cancel timer. - sound._seek = seek; - sound._ended = false; - self._clearTimer(id); - - // Restart the playback if the sound was playing. - if (playing) { - self.play(id, true); - } - - // Update the seek position for HTML5 Audio. - if (!self._webAudio && sound._node) { - sound._node.currentTime = seek; - } - - self._emit('seek', id); - } else { - if (self._webAudio) { - var realTime = self.playing(id) ? Howler.ctx.currentTime - sound._playStart : 0; - var rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0; - return sound._seek + (rateSeek + realTime * Math.abs(sound._rate)); - } else { - return sound._node.currentTime; - } - } - } - - return self; - }, - - /** - * Check if a specific sound is currently playing or not (if id is provided), or check if at least one of the sounds in the group is playing or not. - * @param {Number} id The sound id to check. If none is passed, the whole sound group is checked. - * @return {Boolean} True if playing and false if not. - */ - playing: function(id) { - var self = this; - - // Check the passed sound ID (if any). - if (typeof id === 'number') { - var sound = self._soundById(id); - return sound ? !sound._paused : false; - } - - // Otherwise, loop through all sounds and check if any are playing. - for (var i=0; i= 0) { - Howler._howls.splice(index, 1); - } - } - - // Delete this sound from the cache (if no other Howl is using it). - var remCache = true; - for (i=0; i=0; i--) { - if (!events[i].id || events[i].id === id || event === 'load') { - setTimeout(function(fn) { - fn.call(this, id, msg); - }.bind(self, events[i].fn), 0); - - // If this event was setup with `once`, remove it. - if (events[i].once) { - self.off(event, events[i].fn, events[i].id); - } - } - } - - return self; - }, - - /** - * Queue of actions initiated before the sound has loaded. - * These will be called in sequence, with the next only firing - * after the previous has finished executing (even if async like play). - * @return {Howl} - */ - _loadQueue: function() { - var self = this; - - if (self._queue.length > 0) { - var task = self._queue[0]; - - // don't move onto the next task until this one is done - self.once(task.event, function() { - self._queue.shift(); - self._loadQueue(); - }); - - task.action(); - } - - return self; - }, - - /** - * Fired when playback ends at the end of the duration. - * @param {Sound} sound The sound object to work with. - * @return {Howl} - */ - _ended: function(sound) { - var self = this; - var sprite = sound._sprite; - - // Should this sound loop? - var loop = !!(sound._loop || self._sprite[sprite][2]); - - // Fire the ended event. - self._emit('end', sound._id); - - // Restart the playback for HTML5 Audio loop. - if (!self._webAudio && loop) { - self.stop(sound._id, true).play(sound._id); - } - - // Restart this timer if on a Web Audio loop. - if (self._webAudio && loop) { - self._emit('play', sound._id); - sound._seek = sound._start || 0; - sound._rateSeek = 0; - sound._playStart = Howler.ctx.currentTime; - - var timeout = ((sound._stop - sound._start) * 1000) / Math.abs(sound._rate); - self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); - } - - // Mark the node as paused. - if (self._webAudio && !loop) { - sound._paused = true; - sound._ended = true; - sound._seek = sound._start || 0; - sound._rateSeek = 0; - self._clearTimer(sound._id); - - // Clean up the buffer source. - self._cleanBuffer(sound._node); - - // Attempt to auto-suspend AudioContext if no sounds are still playing. - Howler._autoSuspend(); - } - - // When using a sprite, end the track. - if (!self._webAudio && !loop) { - self.stop(sound._id); - } - - return self; - }, - - /** - * Clear the end timer for a sound playback. - * @param {Number} id The sound ID. - * @return {Howl} - */ - _clearTimer: function(id) { - var self = this; - - if (self._endTimers[id]) { - clearTimeout(self._endTimers[id]); - delete self._endTimers[id]; - } - - return self; - }, - - /** - * Return the sound identified by this ID, or return null. - * @param {Number} id Sound ID - * @return {Object} Sound object or null. - */ - _soundById: function(id) { - var self = this; - - // Loop through all sounds and find the one with this ID. - for (var i=0; i=0; i--) { - if (cnt <= limit) { - return; - } - - if (self._sounds[i]._ended) { - // Disconnect the audio source when using Web Audio. - if (self._webAudio && self._sounds[i]._node) { - self._sounds[i]._node.disconnect(0); - } - - // Remove sounds until we have the pool size. - self._sounds.splice(i, 1); - cnt--; - } - } - }, - - /** - * Get all ID's from the sounds pool. - * @param {Number} id Only return one ID if one is passed. - * @return {Array} Array of IDs. - */ - _getSoundIds: function(id) { - var self = this; - - if (typeof id === 'undefined') { - var ids = []; - for (var i=0; i 0) { - cache[self._src] = buffer; - loadSound(self, buffer); - } - }, function() { - self._emit('loaderror', null, 'Decoding audio data failed.'); - }); - }; - - /** - * Sound is now loaded, so finish setting everything up and fire the loaded event. - * @param {Howl} self - * @param {Object} buffer The decoded buffer sound source. - */ - var loadSound = function(self, buffer) { - // Set the duration. - if (buffer && !self._duration) { - self._duration = buffer.duration; - } - - // Setup a sprite if none is defined. - if (Object.keys(self._sprite).length === 0) { - self._sprite = {__default: [0, self._duration * 1000]}; - } - - // Fire the loaded event. - if (self._state !== 'loaded') { - self._state = 'loaded'; - self._emit('load'); - self._loadQueue(); - } - }; - - /** - * Setup the audio context when available, or switch to HTML5 Audio mode. - */ - var setupAudioContext = function() { - // Check if we are using Web Audio and setup the AudioContext if we are. - try { - if (typeof AudioContext !== 'undefined') { - Howler.ctx = new AudioContext(); - } else if (typeof webkitAudioContext !== 'undefined') { - Howler.ctx = new webkitAudioContext(); - } else { - Howler.usingWebAudio = false; - } - } catch(e) { - Howler.usingWebAudio = false; - } - - // Check if a webview is being used on iOS8 or earlier (rather than the browser). - // If it is, disable Web Audio as it causes crashing. - var iOS = (/iP(hone|od|ad)/.test(Howler._navigator && Howler._navigator.platform)); - var appVersion = Howler._navigator && Howler._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); - var version = appVersion ? parseInt(appVersion[1], 10) : null; - if (iOS && version && version < 9) { - var safari = /safari/.test(Howler._navigator && Howler._navigator.userAgent.toLowerCase()); - if (Howler._navigator && Howler._navigator.standalone && !safari || Howler._navigator && !Howler._navigator.standalone && !safari) { - Howler.usingWebAudio = false; - } - } - - // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage). - if (Howler.usingWebAudio) { - Howler.masterGain = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); - Howler.masterGain.gain.value = 1; - Howler.masterGain.connect(Howler.ctx.destination); - } - - // Re-run the setup on Howler. - Howler._setup(); - }; - - // Add support for AMD (Asynchronous Module Definition) libraries such as require.js. - if (typeof define === 'function' && define.amd) { - define([], function() { - return { - Howler: Howler, - Howl: Howl - }; - }); - } - - // Add support for CommonJS libraries such as browserify. - if (typeof exports !== 'undefined') { - exports.Howler = Howler; - exports.Howl = Howl; - } - - // Define globally in case AMD is not available or unused. - if (typeof window !== 'undefined') { - window.HowlerGlobal = HowlerGlobal; - window.Howler = Howler; - window.Howl = Howl; - window.Sound = Sound; - } else if (typeof global !== 'undefined') { // Add to global in Node.js (for testing, etc). - global.HowlerGlobal = HowlerGlobal; - global.Howler = Howler; - global.Howl = Howl; - global.Sound = Sound; - } -})(); - - -/*! - * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported. - * - * howler.js v2.0.3 - * howlerjs.com - * - * (c) 2013-2017, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - -(function() { - - 'use strict'; - - // Setup default properties. - HowlerGlobal.prototype._pos = [0, 0, 0]; - HowlerGlobal.prototype._orientation = [0, 0, -1, 0, 1, 0]; - - /** Global Methods **/ - /***************************************************************************/ - - /** - * Helper method to update the stereo panning position of all current Howls. - * Future Howls will not use this value unless explicitly set. - * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right. - * @return {Howler/Number} Self or current stereo panning value. - */ - HowlerGlobal.prototype.stereo = function(pan) { - var self = this; - - // Stop right here if not using Web Audio. - if (!self.ctx || !self.ctx.listener) { - return self; - } - - // Loop through all Howls and update their stereo panning. - for (var i=self._howls.length-1; i>=0; i--) { - self._howls[i].stereo(pan); - } - - return self; - }; - - /** - * Get/set the position of the listener in 3D cartesian space. Sounds using - * 3D position will be relative to the listener's position. - * @param {Number} x The x-position of the listener. - * @param {Number} y The y-position of the listener. - * @param {Number} z The z-position of the listener. - * @return {Howler/Array} Self or current listener position. - */ - HowlerGlobal.prototype.pos = function(x, y, z) { - var self = this; - - // Stop right here if not using Web Audio. - if (!self.ctx || !self.ctx.listener) { - return self; - } - - // Set the defaults for optional 'y' & 'z'. - y = (typeof y !== 'number') ? self._pos[1] : y; - z = (typeof z !== 'number') ? self._pos[2] : z; - - if (typeof x === 'number') { - self._pos = [x, y, z]; - self.ctx.listener.setPosition(self._pos[0], self._pos[1], self._pos[2]); - } else { - return self._pos; - } - - return self; - }; - - /** - * Get/set the direction the listener is pointing in the 3D cartesian space. - * A front and up vector must be provided. The front is the direction the - * face of the listener is pointing, and up is the direction the top of the - * listener is pointing. Thus, these values are expected to be at right angles - * from each other. - * @param {Number} x The x-orientation of the listener. - * @param {Number} y The y-orientation of the listener. - * @param {Number} z The z-orientation of the listener. - * @param {Number} xUp The x-orientation of the top of the listener. - * @param {Number} yUp The y-orientation of the top of the listener. - * @param {Number} zUp The z-orientation of the top of the listener. - * @return {Howler/Array} Returns self or the current orientation vectors. - */ - HowlerGlobal.prototype.orientation = function(x, y, z, xUp, yUp, zUp) { - var self = this; - - // Stop right here if not using Web Audio. - if (!self.ctx || !self.ctx.listener) { - return self; - } - - // Set the defaults for optional 'y' & 'z'. - var or = self._orientation; - y = (typeof y !== 'number') ? or[1] : y; - z = (typeof z !== 'number') ? or[2] : z; - xUp = (typeof xUp !== 'number') ? or[3] : xUp; - yUp = (typeof yUp !== 'number') ? or[4] : yUp; - zUp = (typeof zUp !== 'number') ? or[5] : zUp; - - if (typeof x === 'number') { - self._orientation = [x, y, z, xUp, yUp, zUp]; - self.ctx.listener.setOrientation(x, y, z, xUp, yUp, zUp); - } else { - return or; - } - - return self; - }; - - /** Group Methods **/ - /***************************************************************************/ - - /** - * Add new properties to the core init. - * @param {Function} _super Core init method. - * @return {Howl} - */ - Howl.prototype.init = (function(_super) { - return function(o) { - var self = this; - - // Setup user-defined default properties. - self._orientation = o.orientation || [1, 0, 0]; - self._stereo = o.stereo || null; - self._pos = o.pos || null; - self._pannerAttr = { - coneInnerAngle: typeof o.coneInnerAngle !== 'undefined' ? o.coneInnerAngle : 360, - coneOuterAngle: typeof o.coneOuterAngle !== 'undefined' ? o.coneOuterAngle : 360, - coneOuterGain: typeof o.coneOuterGain !== 'undefined' ? o.coneOuterGain : 0, - distanceModel: typeof o.distanceModel !== 'undefined' ? o.distanceModel : 'inverse', - maxDistance: typeof o.maxDistance !== 'undefined' ? o.maxDistance : 10000, - panningModel: typeof o.panningModel !== 'undefined' ? o.panningModel : 'HRTF', - refDistance: typeof o.refDistance !== 'undefined' ? o.refDistance : 1, - rolloffFactor: typeof o.rolloffFactor !== 'undefined' ? o.rolloffFactor : 1 - }; - - // Setup event listeners. - self._onstereo = o.onstereo ? [{fn: o.onstereo}] : []; - self._onpos = o.onpos ? [{fn: o.onpos}] : []; - self._onorientation = o.onorientation ? [{fn: o.onorientation}] : []; - - // Complete initilization with howler.js core's init function. - return _super.call(this, o); - }; - })(Howl.prototype.init); - - /** - * Get/set the stereo panning of the audio source for this sound or all in the group. - * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right. - * @param {Number} id (optional) The sound ID. If none is passed, all in group will be updated. - * @return {Howl/Number} Returns self or the current stereo panning value. - */ - Howl.prototype.stereo = function(pan, id) { - var self = this; - - // Stop right here if not using Web Audio. - if (!self._webAudio) { - return self; - } - - // If the sound hasn't loaded, add it to the load queue to change stereo pan when capable. - if (self._state !== 'loaded') { - self._queue.push({ - event: 'stereo', - action: function() { - self.stereo(pan, id); - } - }); - - return self; - } - - // Check for PannerStereoNode support and fallback to PannerNode if it doesn't exist. - var pannerType = (typeof Howler.ctx.createStereoPanner === 'undefined') ? 'spatial' : 'stereo'; - - // Setup the group's stereo panning if no ID is passed. - if (typeof id === 'undefined') { - // Return the group's stereo panning if no parameters are passed. - if (typeof pan === 'number') { - self._stereo = pan; - self._pos = [pan, 0, 0]; - } else { - return self._stereo; - } - } - - // Change the streo panning of one or all sounds in group. - var ids = self._getSoundIds(id); - for (var i=0; i Returns the group's values. - * pannerAttr(id) -> Returns the sound id's values. - * pannerAttr(o) -> Set's the values of all sounds in this Howl group. - * pannerAttr(o, id) -> Set's the values of passed sound id. - * - * Attributes: - * coneInnerAngle - (360 by default) There will be no volume reduction inside this angle. - * coneOuterAngle - (360 by default) The volume will be reduced to a constant value of - * `coneOuterGain` outside this angle. - * coneOuterGain - (0 by default) The amount of volume reduction outside of `coneOuterAngle`. - * distanceModel - ('inverse' by default) Determines algorithm to use to reduce volume as audio moves - * away from listener. Can be `linear`, `inverse` or `exponential`. - * maxDistance - (10000 by default) Volume won't reduce between source/listener beyond this distance. - * panningModel - ('HRTF' by default) Determines which spatialization algorithm is used to position audio. - * Can be `HRTF` or `equalpower`. - * refDistance - (1 by default) A reference distance for reducing volume as the source - * moves away from the listener. - * rolloffFactor - (1 by default) How quickly the volume reduces as source moves from listener. - * - * @return {Howl/Object} Returns self or current panner attributes. - */ - Howl.prototype.pannerAttr = function() { - var self = this; - var args = arguments; - var o, id, sound; - - // Stop right here if not using Web Audio. - if (!self._webAudio) { - return self; - } - - // Determine the values based on arguments. - if (args.length === 0) { - // Return the group's panner attribute values. - return self._pannerAttr; - } else if (args.length === 1) { - if (typeof args[0] === 'object') { - o = args[0]; - - // Set the grou's panner attribute values. - if (typeof id === 'undefined') { - self._pannerAttr = { - coneInnerAngle: typeof o.coneInnerAngle !== 'undefined' ? o.coneInnerAngle : self._coneInnerAngle, - coneOuterAngle: typeof o.coneOuterAngle !== 'undefined' ? o.coneOuterAngle : self._coneOuterAngle, - coneOuterGain: typeof o.coneOuterGain !== 'undefined' ? o.coneOuterGain : self._coneOuterGain, - distanceModel: typeof o.distanceModel !== 'undefined' ? o.distanceModel : self._distanceModel, - maxDistance: typeof o.maxDistance !== 'undefined' ? o.maxDistance : self._maxDistance, - panningModel: typeof o.panningModel !== 'undefined' ? o.panningModel : self._panningModel, - refDistance: typeof o.refDistance !== 'undefined' ? o.refDistance : self._refDistance, - rolloffFactor: typeof o.rolloffFactor !== 'undefined' ? o.rolloffFactor : self._rolloffFactor - }; - } - } else { - // Return this sound's panner attribute values. - sound = self._soundById(parseInt(args[0], 10)); - return sound ? sound._pannerAttr : self._pannerAttr; - } - } else if (args.length === 2) { - o = args[0]; - id = parseInt(args[1], 10); - } - - // Update the values of the specified sounds. - var ids = self._getSoundIds(id); - for (var i=0; i=0&&e<=1){if(o._volume=e,o._muted)return o;o.usingWebAudio&&o.masterGain.gain.setValueAtTime(e,n.ctx.currentTime);for(var t=0;t=0;o--)e._howls[o].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,_()),e},codecs:function(e){return(this||n)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||n;if(e.state=e.ctx?e.ctx.state||"suspended":"suspended",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{var o=new Audio;void 0===o.oncanplaythrough&&(e._canPlayEvent="canplay")}catch(n){e.noAudio=!0}else e.noAudio=!0;try{var o=new Audio;o.muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||n,o=null;try{o="undefined"!=typeof Audio?new Audio:null}catch(n){return e}if(!o||"function"!=typeof o.canPlayType)return e;var t=o.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator&&e._navigator.userAgent.match(/OPR\/([0-6].)/g),a=r&&parseInt(r[0].split("/")[1],10)<33;return e._codecs={mp3:!(a||!t&&!o.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!t,opus:!!o.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!o.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),aac:!!o.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!o.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(o.canPlayType("audio/x-m4a;")||o.canPlayType("audio/m4a;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(o.canPlayType("audio/x-mp4;")||o.canPlayType("audio/mp4;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),webm:!!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),dolby:!!o.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(o.canPlayType("audio/x-flac;")||o.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_unlockAudio:function(){var e=this||n,o=/iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk|Mobi|Chrome|Safari/i.test(e._navigator&&e._navigator.userAgent);if(!e._audioUnlocked&&e.ctx&&o){e._audioUnlocked=!1,e.autoUnlock=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var t=function(n){for(var o=0;o0?i._seek:t._sprite[e][0]/1e3),s=Math.max(0,(t._sprite[e][0]+t._sprite[e][1])/1e3-_),l=1e3*s/Math.abs(i._rate),c=t._sprite[e][0]/1e3,f=(t._sprite[e][0]+t._sprite[e][1])/1e3,p=!(!i._loop&&!t._sprite[e][2]);i._sprite=e,i._ended=!1;var m=function(){i._paused=!1,i._seek=_,i._start=c,i._stop=f,i._loop=p};if(_>=f)return void t._ended(i);var v=i._node;if(t._webAudio){var h=function(){t._playLock=!1,m(),t._refreshBuffer(i);var e=i._muted||t._muted?0:i._volume;v.gain.setValueAtTime(e,n.ctx.currentTime),i._playStart=n.ctx.currentTime,void 0===v.bufferSource.start?i._loop?v.bufferSource.noteGrainOn(0,_,86400):v.bufferSource.noteGrainOn(0,_,s):i._loop?v.bufferSource.start(0,_,86400):v.bufferSource.start(0,_,s),l!==1/0&&(t._endTimers[i._id]=setTimeout(t._ended.bind(t,i),l)),o||setTimeout(function(){t._emit("play",i._id),t._loadQueue()},0)};"running"===n.state?h():(t._playLock=!0,t.once("resume",h),t._clearTimer(i._id))}else{var y=function(){v.currentTime=_,v.muted=i._muted||t._muted||n._muted||v.muted,v.volume=i._volume*n.volume(),v.playbackRate=i._rate;try{var r=v.play();if(r&&"undefined"!=typeof Promise&&(r instanceof Promise||"function"==typeof r.then)?(t._playLock=!0,m(),r.then(function(){t._playLock=!1,v._unlocked=!0,o||(t._emit("play",i._id),t._loadQueue())}).catch(function(){t._playLock=!1,t._emit("playerror",i._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction."),i._ended=!0,i._paused=!0})):o||(t._playLock=!1,m(),t._emit("play",i._id),t._loadQueue()),v.playbackRate=i._rate,v.paused)return void t._emit("playerror",i._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.");"__default"!==e||i._loop?t._endTimers[i._id]=setTimeout(t._ended.bind(t,i),l):(t._endTimers[i._id]=function(){t._ended(i),v.removeEventListener("ended",t._endTimers[i._id],!1)},v.addEventListener("ended",t._endTimers[i._id],!1))}catch(e){t._emit("playerror",i._id,e)}},g=window&&window.ejecta||!v.readyState&&n._navigator.isCocoonJS;if(v.readyState>=3||g)y();else{t._playLock=!0;var b=function(){y(),v.removeEventListener(n._canPlayEvent,b,!1)};v.addEventListener(n._canPlayEvent,b,!1),t._clearTimer(i._id)}}return i._id},pause:function(e){var n=this;if("loaded"!==n._state||n._playLock)return n._queue.push({event:"pause",action:function(){n.pause(e)}}),n;for(var o=n._getSoundIds(e),t=0;t=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else r.length>=2&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var a;if(!(void 0!==e&&e>=0&&e<=1))return a=o?t._soundById(o):t._sounds[0],a?a._volume:0;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"volume",action:function(){t.volume.apply(t,r)}}),t;void 0===o&&(t._volume=e),o=t._getSoundIds(o);for(var u=0;u0?t/_:t),l=Date.now();e._fadeTo=o,e._interval=setInterval(function(){var r=(Date.now()-l)/t;l=Date.now(),i+=d*r,i=Math.max(0,i),i=Math.min(1,i),i=Math.round(100*i)/100,u._webAudio?e._volume=i:u.volume(i,e._id,!0),a&&(u._volume=i),(on&&i>=o)&&(clearInterval(e._interval),e._interval=null,e._fadeTo=null,u.volume(o,e._id),u._emit("fade",e._id))},s)},_stopFade:function(e){var o=this,t=o._soundById(e);return t&&t._interval&&(o._webAudio&&t._node.gain.cancelScheduledValues(n.ctx.currentTime),clearInterval(t._interval),t._interval=null,o.volume(t._fadeTo,e),t._fadeTo=null,o._emit("fade",e)),o},loop:function(){var e,n,o,t=this,r=arguments;if(0===r.length)return t._loop;if(1===r.length){if("boolean"!=typeof r[0])return!!(o=t._soundById(parseInt(r[0],10)))&&o._loop;e=r[0],t._loop=e}else 2===r.length&&(e=r[0],n=parseInt(r[1],10));for(var a=t._getSoundIds(n),u=0;u=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var i;if("number"!=typeof e)return i=t._soundById(o),i?i._rate:t._rate;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"rate",action:function(){t.rate.apply(t,r)}}),t;void 0===o&&(t._rate=e),o=t._getSoundIds(o);for(var d=0;d=0?o=parseInt(r[0],10):t._sounds.length&&(o=t._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));if(void 0===o)return t;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"seek",action:function(){t.seek.apply(t,r)}}),t;var i=t._soundById(o);if(i){if(!("number"==typeof e&&e>=0)){if(t._webAudio){var d=t.playing(o)?n.ctx.currentTime-i._playStart:0,_=i._rateSeek?i._rateSeek-i._seek:0;return i._seek+(_+d*Math.abs(i._rate))}return i._node.currentTime}var s=t.playing(o);s&&t.pause(o,!0),i._seek=e,i._ended=!1,t._clearTimer(o),t._webAudio||!i._node||isNaN(i._node.duration)||(i._node.currentTime=e);var l=function(){t._emit("seek",o),s&&t.play(o,!0)};if(s&&!t._webAudio){var c=function(){t._playLock?setTimeout(c,0):l()};setTimeout(c,0)}else l()}return t},playing:function(e){var n=this;if("number"==typeof e){var o=n._soundById(e);return!!o&&!o._paused}for(var t=0;t=0&&n._howls.splice(a,1);var u=!0;for(t=0;t=0){u=!1;break}return r&&u&&delete r[e._src],n.noAudio=!1,e._state="unloaded",e._sounds=[],e=null,null},on:function(e,n,o,t){var r=this,a=r["_on"+e];return"function"==typeof n&&a.push(t?{id:o,fn:n,once:t}:{id:o,fn:n}),r},off:function(e,n,o){var t=this,r=t["_on"+e],a=0;if("number"==typeof n&&(o=n,n=null),n||o)for(a=0;a=0;a--)r[a].id&&r[a].id!==n&&"load"!==e||(setTimeout(function(e){e.call(this,n,o)}.bind(t,r[a].fn),0),r[a].once&&t.off(e,r[a].fn,r[a].id));return t._loadQueue(e),t},_loadQueue:function(e){var n=this;if(n._queue.length>0){var o=n._queue[0];o.event===e&&(n._queue.shift(),n._loadQueue()),e||o.action()}return n},_ended:function(e){var o=this,t=e._sprite;if(!o._webAudio&&e._node&&!e._node.paused&&!e._node.ended&&e._node.currentTime=0;t--){if(o<=n)return;e._sounds[t]._ended&&(e._webAudio&&e._sounds[t]._node&&e._sounds[t]._node.disconnect(0),e._sounds.splice(t,1),o--)}}},_getSoundIds:function(e){var n=this;if(void 0===e){for(var o=[],t=0;t=0;if(n._scratchBuffer&&e.bufferSource&&(e.bufferSource.onended=null,e.bufferSource.disconnect(0),t))try{e.bufferSource.buffer=n._scratchBuffer}catch(e){}return e.bufferSource=null,o}};var t=function(e){this._parent=e,this.init()};t.prototype={init:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,o._sounds.push(e),e.create(),e},create:function(){var e=this,o=e._parent,t=n._muted||e._muted||e._parent._muted?0:e._volume;return o._webAudio?(e._node=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),e._node.gain.setValueAtTime(t,n.ctx.currentTime),e._node.paused=!0,e._node.connect(n.masterGain)):(e._node=n._obtainHtml5Audio(),e._errorFn=e._errorListener.bind(e),e._node.addEventListener("error",e._errorFn,!1),e._loadFn=e._loadListener.bind(e),e._node.addEventListener(n._canPlayEvent,e._loadFn,!1),e._node.src=o._src,e._node.preload="auto",e._node.volume=t*n.volume(),e._node.load()),e},reset:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._rateSeek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,e},_errorListener:function(){var e=this;e._parent._emit("loaderror",e._id,e._node.error?e._node.error.code:0),e._node.removeEventListener("error",e._errorFn,!1)},_loadListener:function(){var e=this,o=e._parent;o._duration=Math.ceil(10*e._node.duration)/10,0===Object.keys(o._sprite).length&&(o._sprite={__default:[0,1e3*o._duration]}),"loaded"!==o._state&&(o._state="loaded",o._emit("load"),o._loadQueue()),e._node.removeEventListener(n._canPlayEvent,e._loadFn,!1)}};var r={},a=function(e){var n=e._src;if(r[n])return e._duration=r[n].duration,void d(e);if(/^data:[^;]+;base64,/.test(n)){for(var o=atob(n.split(",")[1]),t=new Uint8Array(o.length),a=0;a0?(r[o._src]=e,d(o,e)):t()};"undefined"!=typeof Promise&&1===n.ctx.decodeAudioData.length?n.ctx.decodeAudioData(e).then(a).catch(t):n.ctx.decodeAudioData(e,a,t)},d=function(e,n){n&&!e._duration&&(e._duration=n.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},_=function(){if(n.usingWebAudio){try{"undefined"!=typeof AudioContext?n.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?n.ctx=new webkitAudioContext:n.usingWebAudio=!1}catch(e){n.usingWebAudio=!1}n.ctx||(n.usingWebAudio=!1);var e=/iP(hone|od|ad)/.test(n._navigator&&n._navigator.platform),o=n._navigator&&n._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),t=o?parseInt(o[1],10):null;if(e&&t&&t<9){var r=/safari/.test(n._navigator&&n._navigator.userAgent.toLowerCase());(n._navigator&&n._navigator.standalone&&!r||n._navigator&&!n._navigator.standalone&&!r)&&(n.usingWebAudio=!1)}n.usingWebAudio&&(n.masterGain=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),n.masterGain.gain.setValueAtTime(n._muted?0:1,n.ctx.currentTime),n.masterGain.connect(n.ctx.destination)),n._setup()}};"function"==typeof define&&define.amd&&define([],function(){return{Howler:n,Howl:o}}),"undefined"!=typeof exports&&(exports.Howler=n,exports.Howl=o),"undefined"!=typeof window?(window.HowlerGlobal=e,window.Howler=n,window.Howl=o,window.Sound=t):"undefined"!=typeof global&&(global.HowlerGlobal=e,global.Howler=n,global.Howl=o,global.Sound=t)}(); +/*! Spatial Plugin */ +!function(){"use strict";HowlerGlobal.prototype._pos=[0,0,0],HowlerGlobal.prototype._orientation=[0,0,-1,0,1,0],HowlerGlobal.prototype.stereo=function(e){var n=this;if(!n.ctx||!n.ctx.listener)return n;for(var t=n._howls.length-1;t>=0;t--)n._howls[t].stereo(e);return n},HowlerGlobal.prototype.pos=function(e,n,t){var r=this;return r.ctx&&r.ctx.listener?(n="number"!=typeof n?r._pos[1]:n,t="number"!=typeof t?r._pos[2]:t,"number"!=typeof e?r._pos:(r._pos=[e,n,t],void 0!==r.ctx.listener.positionX?(r.ctx.listener.positionX.setTargetAtTime(r._pos[0],Howler.ctx.currentTime,.1),r.ctx.listener.positionY.setTargetAtTime(r._pos[1],Howler.ctx.currentTime,.1),r.ctx.listener.positionZ.setTargetAtTime(r._pos[2],Howler.ctx.currentTime,.1)):r.ctx.listener.setPosition(r._pos[0],r._pos[1],r._pos[2]),r)):r},HowlerGlobal.prototype.orientation=function(e,n,t,r,o,i){var a=this;if(!a.ctx||!a.ctx.listener)return a;var s=a._orientation;return n="number"!=typeof n?s[1]:n,t="number"!=typeof t?s[2]:t,r="number"!=typeof r?s[3]:r,o="number"!=typeof o?s[4]:o,i="number"!=typeof i?s[5]:i,"number"!=typeof e?s:(a._orientation=[e,n,t,r,o,i],void 0!==a.ctx.listener.forwardX?(a.ctx.listener.forwardX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.forwardY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.forwardZ.setTargetAtTime(t,Howler.ctx.currentTime,.1),a.ctx.listener.upX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.upY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.upZ.setTargetAtTime(t,Howler.ctx.currentTime,.1)):a.ctx.listener.setOrientation(e,n,t,r,o,i),a)},Howl.prototype.init=function(e){return function(n){var t=this;return t._orientation=n.orientation||[1,0,0],t._stereo=n.stereo||null,t._pos=n.pos||null,t._pannerAttr={coneInnerAngle:void 0!==n.coneInnerAngle?n.coneInnerAngle:360,coneOuterAngle:void 0!==n.coneOuterAngle?n.coneOuterAngle:360,coneOuterGain:void 0!==n.coneOuterGain?n.coneOuterGain:0,distanceModel:void 0!==n.distanceModel?n.distanceModel:"inverse",maxDistance:void 0!==n.maxDistance?n.maxDistance:1e4,panningModel:void 0!==n.panningModel?n.panningModel:"HRTF",refDistance:void 0!==n.refDistance?n.refDistance:1,rolloffFactor:void 0!==n.rolloffFactor?n.rolloffFactor:1},t._onstereo=n.onstereo?[{fn:n.onstereo}]:[],t._onpos=n.onpos?[{fn:n.onpos}]:[],t._onorientation=n.onorientation?[{fn:n.onorientation}]:[],e.call(this,n)}}(Howl.prototype.init),Howl.prototype.stereo=function(n,t){var r=this;if(!r._webAudio)return r;if("loaded"!==r._state)return r._queue.push({event:"stereo",action:function(){r.stereo(n,t)}}),r;var o=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===t){if("number"!=typeof n)return r._stereo;r._stereo=n,r._pos=[n,0,0]}for(var i=r._getSoundIds(t),a=0;a