diff --git a/README.rst b/README.rst
index 6a75b97..4ce90ab 100644
--- a/README.rst
+++ b/README.rst
@@ -11,14 +11,14 @@ Requirements
- youtube-dl
- espeak
-. code::
+.. code::
apt install espeak omxplayer python3.4 python3-pip
pip3 install virtualenv youtube-dl
It is highly recommended to install the soundboard and its dependencies into a virtual environment to keep the system clean. To do so first create a new virtual environment in the desired folder and install the remaining Python dependencies via the `pip` of the virtualenv:
-. code::
+.. code::
virtualenv py
py/bin/pip install -r requirements.txt
@@ -28,7 +28,7 @@ Apache
It is also recommended to use Apache instead of the Python Flask web server.
-. code::
+.. code::
apt install apache2 libapache2-mod-wsgi-py3
@@ -49,7 +49,7 @@ Development/Debugging
For development and debugging the internal web server of Flask can be used e.g. by running following command inside the project folder:
-. code::
+.. code::
FLASK_DEBUG=1 FLASK_APP=soundboard.py py/bin/flask run --host=0.0.0.0
@@ -59,3 +59,28 @@ Production
For production deployment Apache (or other webservers like nginx, lighttpd etc.) should be used.
For Apache a sample configuration is provided in the folder `apache` which can be adapted.
+
+Please keep in mind that you may have to adjust the path to the sound files in your apache config in order to enable local playback on the clients.
+
+Usage
+=====
+
+Users can browse the webinterface, search for sounds and play them on the remote server or their local browser.
+
+Additionally to the normal point and click usage there are also several keyboard shortcuts available.
+
+Keyboard shortcuts
+------------------
+
+- Navigation items, input fields and sound buttons can be selected by using tab and shift+tab
+- esc clears the search field
+- enter plays the selected sound
+- ctrl+k and ctrl+c kill all sounds
+- ctrl+x switches between local and remote playback
+- ctrl+enter kills sounds before the selected gets played
+
+
+Live version
+============
+
+A list of sound requests and already added sounds can be found here: https://pad.wiai.de/p/soundboardTodos
\ No newline at end of file
diff --git a/apache/010-soundboard.conf b/apache/010-soundboard.conf
index 84372a9..03cb961 100644
--- a/apache/010-soundboard.conf
+++ b/apache/010-soundboard.conf
@@ -1,10 +1,10 @@
Listen 5000
- #ServerName example.com
-
WSGIDaemonProcess soundboard user=www-data group=www-data threads=5
WSGIScriptAlias / /var/www/soundboard/soundboard.wsgi
+ Alias /sounds /home/pi/sounds
+
WSGIProcessGroup soundboard
WSGIApplicationGroup %{GLOBAL}
@@ -12,4 +12,14 @@ Listen 5000
Order deny,allow
Allow from all
+
+
+ Options FollowSymLinks
+ AllowOverride All
+ Order deny,allow
+ Allow from all
+ Require all granted
+
+
+
diff --git a/devserver.sh b/devserver.sh
new file mode 100755
index 0000000..460b9de
--- /dev/null
+++ b/devserver.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+basedir=$(dirname "$0")
+
+if [ ! -d "$basedir/py" ]; then
+ echo "Directory \"py\" with a Python virtual environment"
+ echo "does not exist. See README.rst on how to set it up."
+else
+ export FLASK_DEBUG=1
+ export FLASK_APP="$basedir/soundboard.py"
+ "$basedir"/py/bin/flask run
+fi
diff --git a/soundboard.py b/soundboard.py
index 83ed7d5..4707577 100644
--- a/soundboard.py
+++ b/soundboard.py
@@ -3,7 +3,7 @@ import sys
import subprocess
import sqlite3
-from flask import Flask, render_template, request, redirect, url_for, g
+from flask import Flask, render_template, request, redirect, url_for, send_from_directory, g
import config
@@ -66,7 +66,7 @@ def index(sound=None, text=None, video=None):
pitch = request.form.get("pitch", default="")
pitch = pitch if pitch.strip() != "" else "50"
- subprocess.Popen(["espeak", "-v", voice, "-s", speed, "-p", pitch, text], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ subprocess.Popen(["espeak", "-v", voice, "-s", speed, "-p", pitch, text.encode("utf-8")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return redirect("/")
video = request.args.get("video")
@@ -140,3 +140,7 @@ ON tag.id = checked.id""", (sound,))
return redirect("/edit")
return render_template("edit.html", sound=sound, tags=tags)
+
+@app.route("/sounds/")
+def sounds(name):
+ return send_from_directory(config.path, name)
diff --git a/static/favicon/apple-touch-icon-114x114.png b/static/favicon/apple-touch-icon-114x114.png
new file mode 100644
index 0000000..ca8d7b1
Binary files /dev/null and b/static/favicon/apple-touch-icon-114x114.png differ
diff --git a/static/favicon/apple-touch-icon-120x120.png b/static/favicon/apple-touch-icon-120x120.png
new file mode 100644
index 0000000..823c594
Binary files /dev/null and b/static/favicon/apple-touch-icon-120x120.png differ
diff --git a/static/favicon/apple-touch-icon-144x144.png b/static/favicon/apple-touch-icon-144x144.png
new file mode 100644
index 0000000..efbf992
Binary files /dev/null and b/static/favicon/apple-touch-icon-144x144.png differ
diff --git a/static/favicon/apple-touch-icon-152x152.png b/static/favicon/apple-touch-icon-152x152.png
new file mode 100644
index 0000000..25d8de4
Binary files /dev/null and b/static/favicon/apple-touch-icon-152x152.png differ
diff --git a/static/favicon/apple-touch-icon-57x57.png b/static/favicon/apple-touch-icon-57x57.png
new file mode 100644
index 0000000..9076a71
Binary files /dev/null and b/static/favicon/apple-touch-icon-57x57.png differ
diff --git a/static/favicon/apple-touch-icon-60x60.png b/static/favicon/apple-touch-icon-60x60.png
new file mode 100644
index 0000000..c0e1664
Binary files /dev/null and b/static/favicon/apple-touch-icon-60x60.png differ
diff --git a/static/favicon/apple-touch-icon-72x72.png b/static/favicon/apple-touch-icon-72x72.png
new file mode 100644
index 0000000..9786f31
Binary files /dev/null and b/static/favicon/apple-touch-icon-72x72.png differ
diff --git a/static/favicon/apple-touch-icon-76x76.png b/static/favicon/apple-touch-icon-76x76.png
new file mode 100644
index 0000000..1b46ea4
Binary files /dev/null and b/static/favicon/apple-touch-icon-76x76.png differ
diff --git a/static/favicon/favicon-128.png b/static/favicon/favicon-128.png
new file mode 100644
index 0000000..75e7b0f
Binary files /dev/null and b/static/favicon/favicon-128.png differ
diff --git a/static/favicon/favicon-16x16.png b/static/favicon/favicon-16x16.png
new file mode 100644
index 0000000..ec87e33
Binary files /dev/null and b/static/favicon/favicon-16x16.png differ
diff --git a/static/favicon/favicon-196x196.png b/static/favicon/favicon-196x196.png
new file mode 100644
index 0000000..17aa0b9
Binary files /dev/null and b/static/favicon/favicon-196x196.png differ
diff --git a/static/favicon/favicon-32x32.png b/static/favicon/favicon-32x32.png
new file mode 100644
index 0000000..36aa116
Binary files /dev/null and b/static/favicon/favicon-32x32.png differ
diff --git a/static/favicon/favicon-96x96.png b/static/favicon/favicon-96x96.png
new file mode 100644
index 0000000..9e8ee87
Binary files /dev/null and b/static/favicon/favicon-96x96.png differ
diff --git a/static/favicon/favicon.ico b/static/favicon/favicon.ico
new file mode 100644
index 0000000..2d0c58c
Binary files /dev/null and b/static/favicon/favicon.ico differ
diff --git a/static/favicon/mstile-144x144.png b/static/favicon/mstile-144x144.png
new file mode 100644
index 0000000..efbf992
Binary files /dev/null and b/static/favicon/mstile-144x144.png differ
diff --git a/static/favicon/mstile-150x150.png b/static/favicon/mstile-150x150.png
new file mode 100644
index 0000000..2133daa
Binary files /dev/null and b/static/favicon/mstile-150x150.png differ
diff --git a/static/favicon/mstile-310x150.png b/static/favicon/mstile-310x150.png
new file mode 100644
index 0000000..7549627
Binary files /dev/null and b/static/favicon/mstile-310x150.png differ
diff --git a/static/favicon/mstile-310x310.png b/static/favicon/mstile-310x310.png
new file mode 100644
index 0000000..2647bf2
Binary files /dev/null and b/static/favicon/mstile-310x310.png differ
diff --git a/static/favicon/mstile-70x70.png b/static/favicon/mstile-70x70.png
new file mode 100644
index 0000000..75e7b0f
Binary files /dev/null and b/static/favicon/mstile-70x70.png differ
diff --git a/static/howler.js b/static/howler.js
new file mode 100644
index 0000000..dfe2897
--- /dev/null
+++ b/static/howler.js
@@ -0,0 +1,2801 @@
+/*!
+ * howler.js v2.0.4
+ * howlerjs.com
+ *
+ * (c) 2013-2017, James Simpson of GoldFire Studios
+ * goldfirestudios.com
+ *
+ * MIT License
+ */
+
+(function() {
+
+ 'use strict';
+
+ /** Global Methods **/
+ /***************************************************************************/
+
+ /**
+ * Create the global controller. All contained methods and properties apply
+ * to all sounds that are currently playing or will be in the future.
+ */
+ var HowlerGlobal = function() {
+ this.init();
+ };
+ HowlerGlobal.prototype = {
+ /**
+ * Initialize the global Howler object.
+ * @return {Howler}
+ */
+ init: function() {
+ var self = this || Howler;
+
+ // Create a global ID counter.
+ self._counter = 1000;
+
+ // Internal properties.
+ self._codecs = {};
+ self._howls = [];
+ self._muted = false;
+ self._volume = 1;
+ self._canPlayEvent = 'canplaythrough';
+ self._navigator = (typeof window !== 'undefined' && window.navigator) ? window.navigator : null;
+
+ // Public properties.
+ self.masterGain = null;
+ self.noAudio = false;
+ self.usingWebAudio = true;
+ self.autoSuspend = true;
+ self.ctx = null;
+
+ // Set to false to disable the auto iOS enabler.
+ self.mobileAutoEnable = true;
+
+ // Setup the various state values for global tracking.
+ self._setup();
+
+ return self;
+ },
+
+ /**
+ * Get/set the global volume for all sounds.
+ * @param {Float} vol Volume from 0.0 to 1.0.
+ * @return {Howler/Float} Returns self or current volume.
+ */
+ volume: function(vol) {
+ var self = this || Howler;
+ vol = parseFloat(vol);
+
+ // If we don't have an AudioContext created yet, run the setup.
+ if (!self.ctx) {
+ setupAudioContext();
+ }
+
+ if (typeof vol !== 'undefined' && vol >= 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() {
+ // Fix Android can not play in suspend state.
+ Howler._autoResume();
+
+ // 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);
+ }
+
+ // Calling resume() on a stack initiated by user gesture is what actually unlocks the audio on Android Chrome >= 55.
+ if (typeof self.ctx.resume === 'function') {
+ self.ctx.resume();
+ }
+
+ // 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;
+
+ // If we are using IE and there was network latency we may be clipping
+ // audio before it completes playing. Lets check the node to make sure it
+ // believes it has completed, before ending the playback.
+ if (!self._webAudio && self._node && !self._node.ended) {
+ setTimeout(self._ended.bind(self, sound), 100);
+ return self;
+ }
+
+ // 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 = Howler._muted ? 0 : 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.4
+ * 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 a {
+nav a {
background-color: #eee;
border: 1px solid #000;
border-radius: 5px;
@@ -121,12 +136,14 @@ section {
width: 100%;
}
-.reset {
+#reset {
font-size: 2em;
padding-left: 5px;
+ display: inline;
+ vertical-align: middle;
}
-.reset:hover {
+#reset:hover {
cursor: pointer;
}
@@ -172,6 +189,18 @@ nav .extra > a {
.sound a:hover {
background-color: #ff4136;
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6);
+ cursor: pointer;
+}
+
+.sound a:active {
+ background-color: #ff4136;
+ box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6);
+ cursor: pointer;
+}
+
+.sound-pressed {
+ background-color: #ff4136 !important;
+ box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6) !important;
}
.edit {
@@ -184,10 +213,15 @@ nav .extra > a {
box-shadow: inset -5px -5px 5px rgba(0, 0, 0, 0.6);
}
-.table-row {
- display: table-row;
+.unselectable {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
}
-.table-cell {
- display: table-cell;
+#local-mode-button .local {
+ color: red;
}
diff --git a/static/main.js b/static/main.js
index d162601..a2e39a7 100644
--- a/static/main.js
+++ b/static/main.js
@@ -1,3 +1,10 @@
+/* jshint esversion: 6 */
+
+var localModeEnabled = false;
+
+// References to howler objects
+var howlerSounds = [];
+
function ready(fn) {
if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading") {
fn();
@@ -6,6 +13,10 @@ function ready(fn) {
}
}
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
ready(function() {
hideSections();
@@ -29,38 +40,18 @@ ready(function() {
});
var searchfield = document.querySelector("#search");
+ searchfield.focus();
searchFilter(searchfield, ".sound", "inline-block");
searchFilter(searchfield, ".tag", "block");
- var reset = document.querySelector("#sounds .reset");
+ var reset = document.querySelector("#reset");
+ var sounds_nav = document.querySelector("#sounds-nav");
- if (reset !== null) {
- reset.addEventListener("click", function() {
- var buttons = document.querySelectorAll(".sound");
+ reset.addEventListener("click", resetSearch, false);
+ sounds_nav.addEventListener("click", resetSearch, false);
- buttons.forEach(function(item) {
- item.style.display = "inline-block";
- });
-
- searchfield.value = "";
- });
- }
-
- var taginputs = document.querySelectorAll(".taginput");
-
- if (taginputs.length > 0) {
- taginputs.forEach(function(taginput) {
- taginput.addEventListener("keydown", function(e) {
- if (e.target == taginput && (e.which == 13 || e.keyCode == 13)) {
- e.preventDefault();
- } else if (e.which == 32 || e.keyCode == 13) {
- console.log("Space");
- addTag(e.target);
- }
- });
- });
- }
+ addKeyListeners();
});
function searchFilter(searchfield, itemSelector, unHideStyle) {
@@ -74,7 +65,6 @@ function searchFilter(searchfield, itemSelector, unHideStyle) {
items.forEach(function(item) {
var name = item.firstChild.innerHTML;
- console.log(name);
if (name.toLowerCase().indexOf(searchfield.value.toLowerCase()) === -1) {
item.style.display = "none";
@@ -84,6 +74,68 @@ function searchFilter(searchfield, itemSelector, unHideStyle) {
}
}
+/*
+* Adds all necessary KeyListeners to document
+*/
+function addKeyListeners() {
+ // keyboard listener for global key strokes/inputs
+ document.onkeydown = function(evt) {
+ evt = evt || window.event;
+ if (evt.keyCode == 27) {
+ // esc key
+ resetSearch();
+ } else if ((evt.keyCode == 75 || evt.keyCode == 67) && evt.ctrlKey) {
+ // ctrl+k and ctrl+c key binding
+ killAllAudio();
+ } else if (evt.keyCode == 88 && evt.ctrlKey){
+ // ctrl+x key binding
+ toggleLocalMode();
+ }
+ };
+
+ // keyboard listener for playing sounds using enter key
+ var buttons = document.querySelectorAll(".sound");
+
+ buttons.forEach(function(item) {
+ item.firstChild.addEventListener('keypress', async function (e) {
+ var key = e.keyCode;
+ var source = e.target;
+ // keylistener for enter or ctrl+enter (which is k10 on chrome)
+ if (key === 13 || (key === 10 && e.ctrlKey)) {
+ if (e.ctrlKey){
+ killAllAudio();
+ await sleep(100);
+ }
+ source.classList.add("sound-pressed");
+ source.onclick();
+ await sleep(300);
+ source.classList.remove("sound-pressed");
+ }
+ });
+ });
+}
+
+
+function resetSearch() {
+ var searchfield = document.querySelector("#search");
+ var buttons = document.querySelectorAll(".sound");
+
+ buttons.forEach(function(item) {
+ item.style.display = "inline-block";
+ });
+
+ searchfield.value = "";
+ searchfield.focus();
+}
+
+/*
+* Reads the stream url from input with id #streaming-url and forwards it to the server using AJAX.
+*/
+function playStream() {
+ var streamUrl = document.querySelector("#streaming-url").value;
+ ajaxRequest("/?video="+encodeURI(streamUrl));
+}
+
function hideSections() {
var sections = document.querySelectorAll("section");
@@ -91,3 +143,67 @@ function hideSections() {
item.style.display = "none";
});
}
+
+function ajaxRequest(url) {
+ var ajaxRequest;
+ try {
+ ajaxRequest = new XMLHttpRequest();
+ ajaxRequest.open("GET", url, true);
+ ajaxRequest.send(null);
+ } catch (e) {
+ alert("Unfortunately we were unable to handle your request. Please try again later and contact the server administrator if the problem persists.");
+ return false;
+ }
+}
+
+/*
+* Switches between local and remote playback mode.
+* Additionally changes the button text and its color so that the user has a visible feedback.
+*/
+function toggleLocalMode() {
+ toggleButton = document.getElementById("local-mode-button").children[0];
+ if (localModeEnabled) {
+ localModeEnabled = false;
+ toggleButton.classList.remove("local");
+ toggleButton.innerHTML = "[S]";
+ toggleButton.title = "play sounds on server";
+ } else {
+ localModeEnabled = true;
+ toggleButton.classList.add("local");
+ toggleButton.innerHTML = "[L]";
+ toggleButton.title = "play sounds locally";
+ }
+}
+
+/*
+* Either plays the given sound file locally by using howler.js or forwards the request to the remote server using AJAX.
+*/
+function playSound(filename) {
+ if (localModeEnabled){
+ // play local audio using howler.js
+ var sound = new Howl({
+ src: ['/sounds/' + filename]
+ });
+ sound.play();
+ howlerSounds.push(sound);
+ } else {
+ ajaxRequest("/play/" + filename);
+ }
+}
+
+function killAllHowlerAudio() {
+ for (var i = 0; i < howlerSounds.length; i++) {
+ howlerSounds[i].stop();
+ }
+}
+
+/*
+* Kills all local or remote audio, depending on localModeEnabled
+*/
+function killAllAudio() {
+ if (localModeEnabled) {
+ killAllHowlerAudio();
+ } else {
+ ajaxRequest("/?killvideo=yes");
+ }
+}
diff --git a/templates/index.html b/templates/index.html
index 3676f82..62b02ff 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1,28 +1,71 @@
-{% extends "base.html" %}
-{% block content %}
-
-