Skip to content

Commit

Permalink
Merge pull request #800 from tf/audio-api-volume-fading
Browse files Browse the repository at this point in the history
Use Web Audio API for volume fading if available
  • Loading branch information
tf authored Jul 26, 2017
2 parents 0b7a7ed + ef01382 commit 966a98f
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 65 deletions.
28 changes: 28 additions & 0 deletions app/assets/javascripts/pageflow/audio_context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Obtain the globally shared audio context. There can only be a
* limited number of `AudioContext` objects in one page.
*
* @since edge
*/
pageflow.audioContext = {
/**
* @returns [AudioContext]
* Returns `null` if web audio API is not supported or creating
* the context fails.
*/
get: function() {
var AudioContext = window.AudioContext || window.webkitAudioContext;

if (typeof this._audioContext === 'undefined') {
try {
this._audioContext = AudioContext && new AudioContext();
}
catch(e) {
this._audioContext = null;
pageflow.log('Failed to create AudioContext.', {force: true});
}
}

return this._audioContext;
}
};
2 changes: 2 additions & 0 deletions app/assets/javascripts/pageflow/audio_player.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//= require ./audio_player/seek_with_invalid_state_handling
//= require ./audio_player/rewind_method
//= require ./audio_player/pause_in_background
//= require ./audio_player/get_media_element_method

/**
* Playing audio sources
Expand Down Expand Up @@ -63,6 +64,7 @@ pageflow.AudioPlayer = function(sources, options) {

pageflow.AudioPlayer.seekWithInvalidStateHandling(audio);
pageflow.AudioPlayer.rewindMethod(audio);
pageflow.AudioPlayer.getMediaElementMethod(audio);

audio.src = function(sources) {
ready.then(function() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pageflow.AudioPlayer.getMediaElementMethod = function(player) {
player.getMediaElement = function() {
return player.audio.audio;
};
};
1 change: 1 addition & 0 deletions app/assets/javascripts/pageflow/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
//= require ./features
//= require ./audio
//= require ./audio_player
//= require ./audio_context
//= require ./video_player
//= require ./visited
//= require ./print_layout
Expand Down
79 changes: 14 additions & 65 deletions app/assets/javascripts/pageflow/media_player/volume_fading.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,17 @@
pageflow.mediaPlayer.volumeFading = function(player) {
var originalVolume = player.volume;
var fadeVolumeDeferred;
var fadeVolumeInterval;

player.volume = function(value) {
if (typeof value !== 'undefined') {
cancelFadeVolume();
}

return originalVolume.apply(player, arguments);
};

player.fadeVolume = function(value, duration) {
if (!pageflow.browser.has('volume control support')) {
return new jQuery.Deferred().resolve().promise();
}

cancelFadeVolume();

return new $.Deferred(function(deferred) {
var resolution = 10;
var startValue = volume();
var steps = duration / resolution;
var leap = (value - startValue) / steps;

if (value === startValue) {
deferred.resolve();
}
else {
fadeVolumeDeferred = deferred;
fadeVolumeInterval = setInterval(function() {
volume(volume() + leap);

if ((volume() >= value && value >= startValue) ||
(volume() <= value && value <= startValue)) {
//= require_self
//= require_tree ./volume_fading

resolveFadeVolume();
}
}, resolution);
}
});

function volume(/* arguments */) {
return originalVolume.apply(player, arguments);
}
};

player.one('dispose', cancelFadeVolume);

function resolveFadeVolume() {
clearInterval(fadeVolumeInterval);
fadeVolumeDeferred.resolve();

fadeVolumeInterval = null;
fadeVolumeDeferred = null;
pageflow.mediaPlayer.volumeFading = function(player) {
if (!pageflow.browser.has('volume control support')) {
return pageflow.mediaPlayer.volumeFading.noop(player);
}

function cancelFadeVolume() {
if (fadeVolumeInterval) {
clearInterval(fadeVolumeInterval);
fadeVolumeDeferred.reject();

fadeVolumeInterval = null;
fadeVolumeDeferred = null;
}
else if (pageflow.audioContext.get()) {
return pageflow.mediaPlayer.volumeFading.webAudio(
player,
pageflow.audioContext.get()
);
}
else {
return pageflow.mediaPlayer.volumeFading.interval(player);
}
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
pageflow.mediaPlayer.volumeFading.interval = function(player) {
var originalVolume = player.volume;

var fadeVolumeDeferred;
var fadeVolumeInterval;

player.volume = function(value) {
if (typeof value !== 'undefined') {
cancelFadeVolume();
}

return originalVolume.apply(player, arguments);
};

player.fadeVolume = function(value, duration) {
cancelFadeVolume();

return new $.Deferred(function(deferred) {
var resolution = 10;
var startValue = volume();
var steps = duration / resolution;
var leap = (value - startValue) / steps;

if (value === startValue) {
deferred.resolve();
}
else {
fadeVolumeDeferred = deferred;
fadeVolumeInterval = setInterval(function() {
volume(volume() + leap);

if ((volume() >= value && value >= startValue) ||
(volume() <= value && value <= startValue)) {

resolveFadeVolume();
}
}, resolution);
}
});
};

player.one('dispose', cancelFadeVolume);

function volume(/* arguments */) {
return originalVolume.apply(player, arguments);
}

function resolveFadeVolume() {
clearInterval(fadeVolumeInterval);
fadeVolumeDeferred.resolve();

fadeVolumeInterval = null;
fadeVolumeDeferred = null;
}

function cancelFadeVolume() {
if (fadeVolumeInterval) {
clearInterval(fadeVolumeInterval);
fadeVolumeDeferred.reject();

fadeVolumeInterval = null;
fadeVolumeDeferred = null;
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pageflow.mediaPlayer.volumeFading.noop = function(player) {
player.fadeVolume = function(value, duration) {
return new jQuery.Deferred().resolve().promise();
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
pageflow.mediaPlayer.volumeFading.webAudio = function(player, audioContext) {
var gainNode;

var currentDeferred;
var currentTimeout;

var currentValue = 1;

var lastStartTime;
var lastDuration;
var lastStartValue;

var allowedMinValue = 0.000001;

player.volume = function(value) {
ensureGainNode();

if (typeof value !== 'undefined') {
cancel();
currentValue = ensureInAllowedRange(value);

return gainNode.gain.setValueAtTime(currentValue,
audioContext.currentTime);
}

return Math.round(currentValue * 100) / 100;
};

player.fadeVolume = function(value, duration) {
ensureGainNode();

cancel();
recordFadeStart(duration);

currentValue = ensureInAllowedRange(value);

gainNode.gain.setValueAtTime(lastStartValue, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(currentValue,
audioContext.currentTime + duration / 1000);

return new $.Deferred(function(deferred) {
currentTimeout = setTimeout(resolve, duration);
currentDeferred = deferred;
}).promise();
};

player.one('dispose', cancel);

function ensureGainNode() {
if (!gainNode) {
gainNode = audioContext.createGain();

var source = audioContext.createMediaElementSource(player.getMediaElement());

source.connect(gainNode);
gainNode.connect(audioContext.destination);
}
}

function resolve() {
clearTimeout(currentTimeout);
currentDeferred.resolve();

currentTimeout = null;
currentDeferred = null;
}

function cancel() {
if (currentDeferred) {
gainNode.gain.cancelScheduledValues(audioContext.currentTime);

clearTimeout(currentTimeout);
currentDeferred.reject();

currentTimeout = null;
currentDeferred = null;

updateCurrentValueFromComputedValue();
}
}

function recordFadeStart(duration) {
lastStartTime = audioContext.currentTime;
lastStartValue = currentValue;
lastDuration = duration;
}

function updateCurrentValueFromComputedValue() {
// Firefox 54 on Ubuntu does not provide computed values when gain
// was changed via one of the scheduling methods. Instead
// gain.value always reports 1. Interpolate manually do determine
// how far the fade was performed before cancel was called.
if (gainNode.gain.value == 1) {
var performedDuration = (audioContext.currentTime - lastStartTime) * 1000;
var lastDelta = currentValue - lastStartValue;

currentValue = ensureInAllowedRange(
lastStartValue + (performedDuration / lastDuration * lastDelta)
);
}
else {
currentValue = gainNode.gain.value;
}
}

function ensureInAllowedRange(value) {
return value < allowedMinValue ? allowedMinValue : value;
}
};
2 changes: 2 additions & 0 deletions app/assets/javascripts/pageflow/video_player.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//= require ./video_player/filter_sources
//= require ./video_player/lazy
//= require ./video_player/cue_settings_methods
//= require ./video_player/get_media_element_method

pageflow.VideoPlayer = function(element, options) {
options = options || {};
Expand All @@ -23,6 +24,7 @@ pageflow.VideoPlayer = function(element, options) {

pageflow.VideoPlayer.prebuffering(player);
pageflow.VideoPlayer.cueSettingsMethods(player);
pageflow.VideoPlayer.getMediaElementMethod(player);

if (options.mediaEvents) {
pageflow.VideoPlayer.mediaEvents(player, options.context);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pageflow.VideoPlayer.getMediaElementMethod = function(player) {
player.getMediaElement = function() {
return player.tech({IWillNotUseThisInPlugins: true}).el();
};
};
3 changes: 3 additions & 0 deletions vendor/assets/javascripts/audio5.min.js
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,9 @@
this.audio.autoplay = false;
this.audio.preload = 'auto';
this.audio.autobuffer = true;

this.audio.setAttribute('crossorigin', 'anonymous');

this.bindEvents();
},
destroyAudio: function(){
Expand Down

0 comments on commit 966a98f

Please sign in to comment.