Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions src/js/control-bar/volume-control/volume-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Component from '../../component.js';
import * as Dom from '../../utils/dom.js';
import {clamp} from '../../utils/num.js';
import {IS_IOS, IS_ANDROID} from '../../utils/browser.js';
import {LinearVolumeTransfer, LogarithmicVolumeTransfer} from '../../utils/volume-transfer.js';

/** @import Player from '../../player' */

Expand All @@ -31,6 +32,7 @@ class VolumeBar extends Slider {
*/
constructor(player, options) {
super(player, options);
this.initVolumeTransfer_();
this.on('slideractive', (e) => this.updateLastVolume_(e));
this.on(player, 'volumechange', (e) => this.updateARIAAttributes(e));
player.ready(() => this.updateARIAAttributes());
Expand Down Expand Up @@ -98,7 +100,11 @@ class VolumeBar extends Slider {
}

this.checkMuted();
this.player_.volume(this.calculateDistance(event));

const sliderPosition = this.calculateDistance(event);
const linearVolume = this.volumeTransfer_.sliderToVolume(sliderPosition);

this.player_.volume(linearVolume);
}

/**
Expand All @@ -120,23 +126,45 @@ class VolumeBar extends Slider {
if (this.player_.muted()) {
return 0;
}
return this.player_.volume();

const linearVolume = this.player_.volume();

return this.volumeTransfer_.volumeToSlider(linearVolume);
}

/**
* Increase volume level for keyboard users
*/
stepForward() {
this.checkMuted();
this.player_.volume(this.player_.volume() + 0.1);

const currentSlider = this.getPercent();

const newSlider = Math.min(currentSlider + 0.1, 1);

const linearVolume = this.volumeTransfer_.sliderToVolume(newSlider);

this.player_.volume(linearVolume);
}

/**
* Decrease volume level for keyboard users
*/
stepBack() {
this.checkMuted();
this.player_.volume(this.player_.volume() - 0.1);

const currentSlider = this.getPercent();

const newSlider = Math.max(currentSlider - 0.1, 0);

if (newSlider < 0.05) {
this.player_.volume(0);
return;
}

const linearVolume = this.volumeTransfer_.sliderToVolume(newSlider);

this.player_.volume(linearVolume);
}

/**
Expand Down Expand Up @@ -181,6 +209,20 @@ class VolumeBar extends Slider {
});
}

/**
* Initialize the volume transfer function
*
* @private
*/
initVolumeTransfer_() {
if (this.player_.options_.logarithmicVolume) {
const dbRange = this.player_.options_.logarithmicVolumeRange;

this.volumeTransfer_ = new LogarithmicVolumeTransfer(dbRange);
} else {
this.volumeTransfer_ = new LinearVolumeTransfer();
}
}
}

/**
Expand Down
127 changes: 127 additions & 0 deletions src/js/utils/volume-transfer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* Base class for volume transfer functions.
*
* Volume transfer functions convert between slider position (UI space) and
* player volume (audio space). This allows different scaling behaviors like
* linear or logarithmic (decibel-based) volume control.
*/
class VolumeTransfer {
/**
* Convert slider position to player volume.
*
* @param {number} sliderPosition - Slider position from 0-1
* @return {number} Player volume from 0-1
*/
sliderToVolume(sliderPosition) {
throw new Error('Must be implemented by subclass');
}

/**
* Convert player volume to slider position.
*
* @param {number} volume - Player volume from 0-1
* @return {number} Slider position from 0-1
*/
volumeToSlider(volume) {
throw new Error('Must be implemented by subclass');
}
}

/**
* Linear volume transfer - direct 1:1 mapping between slider and volume.
*
* This is the default behavior where moving the slider linearly adjusts
* the volume linearly. Simple but may not match human perception of loudness.
*/
class LinearVolumeTransfer extends VolumeTransfer {
/**
* Convert slider position to player volume (1:1 mapping).
*
* @param {number} sliderPosition - Slider position from 0-1
* @return {number} Player volume from 0-1
*/
sliderToVolume(sliderPosition) {
return sliderPosition;
}

/**
* Convert player volume to slider position (1:1 mapping).
*
* @param {number} volume - Player volume from 0-1
* @return {number} Slider position from 0-1
*/
volumeToSlider(volume) {
return volume;
}
}

/**
* Logarithmic volume transfer using decibel scaling.
*
* Provides exponential volume changes as the slider moves linearly, which
* better matches human perception of loudness. Uses decibel (dB) scaling
* where volume = 10^(dB/20).
*/
class LogarithmicVolumeTransfer extends VolumeTransfer {
/**
* Creates a logarithmic volume transfer function.
*
* @param {number} [dbRange=50] - The decibel range for the transfer function.
* Larger values create a more dramatic curve. Typical range: 40-60 dB.
*/
constructor(dbRange = 50) {
super();
this.dbRange = dbRange;
this.offset = Math.pow(10, -dbRange / 20);
}

/**
* Convert slider position to player volume using logarithmic scaling.
*
* Applies exponential scaling so that linear slider movement produces
* logarithmic volume changes, matching human loudness perception.
*
* @param {number} sliderPosition - Slider position from 0-1
* @return {number} Player volume from 0-1
*/
sliderToVolume(sliderPosition) {
if (sliderPosition <= 0) {
return 0;
}

if (sliderPosition >= 1) {
return 1;
}

const dB = sliderPosition * this.dbRange - this.dbRange;

return Math.pow(10, dB / 20) * (1 + this.offset);
}

/**
* Convert player volume to slider position using logarithmic scaling.
*
* Inverse of sliderToVolume - converts linear volume back to the
* corresponding logarithmic slider position.
*
* @param {number} volume - Player volume from 0-1
* @return {number} Slider position from 0-1
*/
volumeToSlider(volume) {
if (volume <= 0) {
return 0;
}

if (volume >= 1) {
return 1;
}

const dB = 20 * Math.log10(volume);
const position = (dB + this.dbRange) / this.dbRange;

return position;
}
}

export default VolumeTransfer;
export { LinearVolumeTransfer, LogarithmicVolumeTransfer };
Loading