From 40bfe681902e43e2217b8218a03a9b97c276b49a Mon Sep 17 00:00:00 2001 From: Chris Triantafilis Date: Sun, 16 Nov 2025 13:49:18 -0500 Subject: [PATCH 1/5] introduce volume-transfer classes and add logarithmicVolume option --- .../control-bar/volume-control/volume-bar.js | 50 ++++++++- src/js/utils/volume-transfer.js | 105 ++++++++++++++++++ 2 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 src/js/utils/volume-transfer.js diff --git a/src/js/control-bar/volume-control/volume-bar.js b/src/js/control-bar/volume-control/volume-bar.js index 4e2a02c3fd..2f8fff709c 100644 --- a/src/js/control-bar/volume-control/volume-bar.js +++ b/src/js/control-bar/volume-control/volume-bar.js @@ -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' */ @@ -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()); @@ -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_.toLinear(sliderPosition); + + this.player_.volume(linearVolume); } /** @@ -120,7 +126,10 @@ class VolumeBar extends Slider { if (this.player_.muted()) { return 0; } - return this.player_.volume(); + + const linearVolume = this.player_.volume(); + + return this.volumeTransfer_.toLogarithmic(linearVolume); } /** @@ -128,7 +137,14 @@ class VolumeBar extends Slider { */ 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_.toLinear(newSlider); + + this.player_.volume(linearVolume); } /** @@ -136,7 +152,19 @@ class VolumeBar extends Slider { */ 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_.toLinear(newSlider); + + this.player_.volume(linearVolume); } /** @@ -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(); + } + } } /** diff --git a/src/js/utils/volume-transfer.js b/src/js/utils/volume-transfer.js new file mode 100644 index 0000000000..5520212c13 --- /dev/null +++ b/src/js/utils/volume-transfer.js @@ -0,0 +1,105 @@ +/** + * Base class for volume transfer functions + */ +class VolumeTransfer { + /** + * Convert logarithmic (slider) to linear (tech) + * + * @param {number} logarithmic - Volume from 0-1 + * @return {number} Linear volume from 0-1 + */ + toLinear(logarithmic) { + throw new Error('Must be implemented by subclass'); + } + + /** + * Convert linear (tech) to logarithmic (slider) + * + * @param {number} linear - Volume from 0-1 + * @return {number} Logarithmic volume from 0-1 + */ + toLogarithmic(linear) { + throw new Error('Must be implemented by subclass'); + } + + getName() { + return 'base'; + } +} + +/** + * Linear transfer - no conversion (current default behavior) + */ +class LinearVolumeTransfer extends VolumeTransfer { + toLinear(value) { + return value; + } + + toLogarithmic(value) { + return value; + } + + getName() { + return 'linear'; + } +} + +/** + * Logarithmic transfer using decibel scaling + */ +class LogarithmicVolumeTransfer extends VolumeTransfer { + constructor(dbRange = 50) { + super(); + this.dbRange = dbRange; + this.offset = Math.pow(10, -dbRange / 20); + } + + /** + * Convert logarithmic slider position to linear volume for tech + * + * @param {number} sliderPosition - Slider position (0-1) + * @return {number} Linear volume (0-1) + */ + toLinear(sliderPosition) { + if (sliderPosition <= 0) { + return 0; + } + + if (sliderPosition >= 1) { + return 1; + } + + const dB = sliderPosition * this.dbRange - this.dbRange; + const linear = Math.pow(10, dB / 20); + + return linear; + } + + /** + * Convert linear volume from tech to logarithmic slider position + * + * @param {number} linear - Linear volume (0-1) + * @return {number} Slider position (0-1) + */ + toLogarithmic(linear) { + if (linear <= 0) { + return 0; + } + + if (linear >= 1) { + return 1; + } + + const dB = 20 * Math.log10(linear); + const position = (dB + this.dbRange) / this.dbRange; + + return position; + } + + getName() { + return 'logarithmic'; + } +} + +export default VolumeTransfer; +export { LinearVolumeTransfer, LogarithmicVolumeTransfer }; From f5da798a884f560e017b76948e5b19ab3154a10f Mon Sep 17 00:00:00 2001 From: Chris Triantafilis Date: Sun, 16 Nov 2025 16:18:35 -0500 Subject: [PATCH 2/5] Setup volume-transfer tests --- test/unit/tech/volume-transfer.test.js | 190 +++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 test/unit/tech/volume-transfer.test.js diff --git a/test/unit/tech/volume-transfer.test.js b/test/unit/tech/volume-transfer.test.js new file mode 100644 index 0000000000..922d2390a5 --- /dev/null +++ b/test/unit/tech/volume-transfer.test.js @@ -0,0 +1,190 @@ +/* eslint-env qunit */ +import VolumeTransfer, { + LinearVolumeTransfer, + LogarithmicVolumeTransfer +} from '../../../src/js/utils/volume-transfer.js'; + +import QUnit from 'qunit'; + +QUnit.module('VolumeTransfer'); + +// Base class tests +QUnit.test('VolumeTransfer base class throws errors', function(assert) { + const transfer = new VolumeTransfer(); + + assert.throws( + () => transfer.toLinear(0.5), + /Must be implemented by subclass/, + 'toLinear throws error on base class' + ); + + assert.throws( + () => transfer.toLogarithmic(0.5), + /Must be implemented by subclass/, + 'toLogarithmic throws error on base class' + ); + + assert.strictEqual( + transfer.getName(), + 'base', + 'getName returns "base"' + ); +}); + +QUnit.test('LinearVolumeTransfer is identity function', function(assert) { + const transfer = new LinearVolumeTransfer(); + + [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0].forEach(value => { + assert.strictEqual( + transfer.toLinear(value), + value, + `toLinear(${value}) returns ${value}` + ); + + assert.strictEqual( + transfer.toLogarithmic(value), + value, + `toLogarithmic(${value}) returns ${value}` + ); + }); + + assert.strictEqual( + transfer.getName(), + 'linear', + 'getName returns "linear"' + ); +}); + +QUnit.test('LogarithmicVolumeTransfer constructor sets dbRange and offset', function(assert) { + const transfer1 = new LogarithmicVolumeTransfer(); + + assert.strictEqual(transfer1.dbRange, 50, 'default dbRange is 50'); + assert.ok(transfer1.offset > 0, 'offset is calculated and > 0'); + + const transfer2 = new LogarithmicVolumeTransfer(60); + + assert.strictEqual(transfer2.dbRange, 60, 'custom dbRange is set'); + + assert.strictEqual( + transfer1.getName(), + 'logarithmic', + 'getName returns "logarithmic"' + ); +}); + +QUnit.test('LogarithmicVolumeTransfer passes through (0,0)', function(assert) { + const transfer = new LogarithmicVolumeTransfer(50); + + assert.strictEqual( + transfer.toLinear(0), + 0, + 'toLinear(0) returns exactly 0' + ); + + assert.strictEqual( + transfer.toLogarithmic(0), + 0, + 'toLogarithmic(0) returns exactly 0' + ); +}); + +QUnit.test('LogarithmicVolumeTransfer passes through (1,1)', function(assert) { + const transfer = new LogarithmicVolumeTransfer(50); + + assert.strictEqual( + transfer.toLinear(1), + 1, + 'toLinear(1) returns exactly 1' + ); + + assert.strictEqual( + transfer.toLogarithmic(1), + 1, + 'toLogarithmic(1) returns exactly 1' + ); +}); + +QUnit.test('LogarithmicVolumeTransfer is invertible', function(assert) { + const transfer = new LogarithmicVolumeTransfer(50); + + [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0].forEach(slider => { + const linear = transfer.toLinear(slider); + const back = transfer.toLogarithmic(linear); + + assert.close( + back, + slider, + 0.00001, + `Round trip for ${slider}: ${slider} -> ${linear.toFixed(4)} -> ${back.toFixed(4)}` + ); + }); +}); + +QUnit.test('LogarithmicVolumeTransfer provides finer control at low volumes', function(assert) { + const transfer = new LogarithmicVolumeTransfer(50); + + const linear50 = transfer.toLinear(0.5); + + assert.ok( + linear50 < 0.1, + `Slider at 50% gives linear < 10%: ${(linear50 * 100).toFixed(2)}%` + ); + + const linear25 = transfer.toLinear(0.25); + + assert.ok( + linear25 < 0.01, + `Slider at 25% gives linear < 1%: ${(linear25 * 100).toFixed(2)}%` + ); + + const delta1 = transfer.toLinear(0.1) - transfer.toLinear(0); + const delta2 = transfer.toLinear(0.2) - transfer.toLinear(0.1); + + assert.ok( + delta1 < 0.01 && delta2 < 0.01, + 'Small slider movements at low volumes give small linear changes' + ); +}); + +QUnit.test('LogarithmicVolumeTransfer with different dbRanges', function(assert) { + const transfer40 = new LogarithmicVolumeTransfer(40); + const transfer50 = new LogarithmicVolumeTransfer(50); + const transfer60 = new LogarithmicVolumeTransfer(60); + + const linear40 = transfer40.toLinear(0.5); + const linear50 = transfer50.toLinear(0.5); + const linear60 = transfer60.toLinear(0.5); + + assert.ok( + linear40 > linear50 && linear50 > linear60, + `Higher dbRange gives more control at low volumes: ${linear40.toFixed(4)} > ${linear50.toFixed(4)} > ${linear60.toFixed(4)}` + ); +}); + +QUnit.test('LogarithmicVolumeTransfer handles edge cases', function(assert) { + const transfer = new LogarithmicVolumeTransfer(50); + + assert.strictEqual( + transfer.toLinear(-0.1), + 0, + 'Negative slider values return 0' + ); + + assert.strictEqual( + transfer.toLinear(1.1), + 1, + 'Slider values > 1 return 1' + ); + + assert.strictEqual( + transfer.toLogarithmic(-0.1), + 0, + 'Negative linear values return 0' + ); + + assert.strictEqual( + transfer.toLogarithmic(1.1), + 1, + 'Linear values > 1 return 1' + ); +}); From 83a499b08b203b3f367b82c14cba2a0f9393b0ff Mon Sep 17 00:00:00 2001 From: Chris Triantafilis Date: Mon, 17 Nov 2025 19:25:18 -0500 Subject: [PATCH 3/5] Update VolumeTransfer function names --- .../control-bar/volume-control/volume-bar.js | 8 +- src/js/utils/volume-transfer.js | 102 ++++++++++------- test/unit/tech/volume-transfer.test.js | 107 +++++------------- 3 files changed, 97 insertions(+), 120 deletions(-) diff --git a/src/js/control-bar/volume-control/volume-bar.js b/src/js/control-bar/volume-control/volume-bar.js index 2f8fff709c..5be7ec3e8b 100644 --- a/src/js/control-bar/volume-control/volume-bar.js +++ b/src/js/control-bar/volume-control/volume-bar.js @@ -102,7 +102,7 @@ class VolumeBar extends Slider { this.checkMuted(); const sliderPosition = this.calculateDistance(event); - const linearVolume = this.volumeTransfer_.toLinear(sliderPosition); + const linearVolume = this.volumeTransfer_.sliderToVolume(sliderPosition); this.player_.volume(linearVolume); } @@ -129,7 +129,7 @@ class VolumeBar extends Slider { const linearVolume = this.player_.volume(); - return this.volumeTransfer_.toLogarithmic(linearVolume); + return this.volumeTransfer_.volumeToSlider(linearVolume); } /** @@ -142,7 +142,7 @@ class VolumeBar extends Slider { const newSlider = Math.min(currentSlider + 0.1, 1); - const linearVolume = this.volumeTransfer_.toLinear(newSlider); + const linearVolume = this.volumeTransfer_.sliderToVolume(newSlider); this.player_.volume(linearVolume); } @@ -162,7 +162,7 @@ class VolumeBar extends Slider { return; } - const linearVolume = this.volumeTransfer_.toLinear(newSlider); + const linearVolume = this.volumeTransfer_.sliderToVolume(newSlider); this.player_.volume(linearVolume); } diff --git a/src/js/utils/volume-transfer.js b/src/js/utils/volume-transfer.js index 5520212c13..b56779d1df 100644 --- a/src/js/utils/volume-transfer.js +++ b/src/js/utils/volume-transfer.js @@ -1,53 +1,74 @@ /** - * Base class for volume transfer functions + * 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 logarithmic (slider) to linear (tech) + * Convert slider position to player volume. * - * @param {number} logarithmic - Volume from 0-1 - * @return {number} Linear volume from 0-1 + * @param {number} sliderPosition - Slider position from 0-1 + * @return {number} Player volume from 0-1 */ - toLinear(logarithmic) { + sliderToVolume(sliderPosition) { throw new Error('Must be implemented by subclass'); } /** - * Convert linear (tech) to logarithmic (slider) + * Convert player volume to slider position. * - * @param {number} linear - Volume from 0-1 - * @return {number} Logarithmic volume from 0-1 + * @param {number} volume - Player volume from 0-1 + * @return {number} Slider position from 0-1 */ - toLogarithmic(linear) { + volumeToSlider(volume) { throw new Error('Must be implemented by subclass'); } - - getName() { - return 'base'; - } } /** - * Linear transfer - no conversion (current default behavior) + * 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 { - toLinear(value) { - return value; - } - - toLogarithmic(value) { - return value; + /** + * 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; } - getName() { - return 'linear'; + /** + * 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 transfer using decibel scaling + * 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; @@ -55,12 +76,15 @@ class LogarithmicVolumeTransfer extends VolumeTransfer { } /** - * Convert logarithmic slider position to linear volume for tech + * Convert slider position to player volume using logarithmic scaling. * - * @param {number} sliderPosition - Slider position (0-1) - * @return {number} Linear volume (0-1) + * 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 */ - toLinear(sliderPosition) { + sliderToVolume(sliderPosition) { if (sliderPosition <= 0) { return 0; } @@ -70,35 +94,33 @@ class LogarithmicVolumeTransfer extends VolumeTransfer { } const dB = sliderPosition * this.dbRange - this.dbRange; - const linear = Math.pow(10, dB / 20); - return linear; + return Math.pow(10, dB / 20) * (1 + this.offset); } /** - * Convert linear volume from tech to logarithmic slider position + * Convert player volume to slider position using logarithmic scaling. + * + * Inverse of sliderToVolume - converts linear volume back to the + * corresponding logarithmic slider position. * - * @param {number} linear - Linear volume (0-1) - * @return {number} Slider position (0-1) + * @param {number} volume - Player volume from 0-1 + * @return {number} Slider position from 0-1 */ - toLogarithmic(linear) { - if (linear <= 0) { + volumeToSlider(volume) { + if (volume <= 0) { return 0; } - if (linear >= 1) { + if (volume >= 1) { return 1; } - const dB = 20 * Math.log10(linear); + const dB = 20 * Math.log10(volume); const position = (dB + this.dbRange) / this.dbRange; return position; } - - getName() { - return 'logarithmic'; - } } export default VolumeTransfer; diff --git a/test/unit/tech/volume-transfer.test.js b/test/unit/tech/volume-transfer.test.js index 922d2390a5..d9b02098c0 100644 --- a/test/unit/tech/volume-transfer.test.js +++ b/test/unit/tech/volume-transfer.test.js @@ -8,26 +8,19 @@ import QUnit from 'qunit'; QUnit.module('VolumeTransfer'); -// Base class tests QUnit.test('VolumeTransfer base class throws errors', function(assert) { const transfer = new VolumeTransfer(); assert.throws( - () => transfer.toLinear(0.5), + () => transfer.sliderToVolume(0.5), /Must be implemented by subclass/, - 'toLinear throws error on base class' + 'sliderToVolume throws error on base class' ); assert.throws( - () => transfer.toLogarithmic(0.5), + () => transfer.volumeToSlider(0.5), /Must be implemented by subclass/, - 'toLogarithmic throws error on base class' - ); - - assert.strictEqual( - transfer.getName(), - 'base', - 'getName returns "base"' + 'volumeToSlider throws error on base class' ); }); @@ -36,23 +29,17 @@ QUnit.test('LinearVolumeTransfer is identity function', function(assert) { [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0].forEach(value => { assert.strictEqual( - transfer.toLinear(value), + transfer.sliderToVolume(value), value, - `toLinear(${value}) returns ${value}` + `sliderToVolume(${value}) returns ${value}` ); assert.strictEqual( - transfer.toLogarithmic(value), + transfer.volumeToSlider(value), value, - `toLogarithmic(${value}) returns ${value}` + `volumeToSlider(${value}) returns ${value}` ); }); - - assert.strictEqual( - transfer.getName(), - 'linear', - 'getName returns "linear"' - ); }); QUnit.test('LogarithmicVolumeTransfer constructor sets dbRange and offset', function(assert) { @@ -64,27 +51,21 @@ QUnit.test('LogarithmicVolumeTransfer constructor sets dbRange and offset', func const transfer2 = new LogarithmicVolumeTransfer(60); assert.strictEqual(transfer2.dbRange, 60, 'custom dbRange is set'); - - assert.strictEqual( - transfer1.getName(), - 'logarithmic', - 'getName returns "logarithmic"' - ); }); QUnit.test('LogarithmicVolumeTransfer passes through (0,0)', function(assert) { const transfer = new LogarithmicVolumeTransfer(50); assert.strictEqual( - transfer.toLinear(0), + transfer.sliderToVolume(0), 0, - 'toLinear(0) returns exactly 0' + 'sliderToVolume(0) returns exactly 0' ); assert.strictEqual( - transfer.toLogarithmic(0), + transfer.volumeToSlider(0), 0, - 'toLogarithmic(0) returns exactly 0' + 'volumeToSlider(0) returns exactly 0' ); }); @@ -92,68 +73,42 @@ QUnit.test('LogarithmicVolumeTransfer passes through (1,1)', function(assert) { const transfer = new LogarithmicVolumeTransfer(50); assert.strictEqual( - transfer.toLinear(1), + transfer.sliderToVolume(1), 1, - 'toLinear(1) returns exactly 1' + 'sliderToVolume(1) returns exactly 1' ); assert.strictEqual( - transfer.toLogarithmic(1), + transfer.volumeToSlider(1), 1, - 'toLogarithmic(1) returns exactly 1' + 'volumeToSlider(1) returns exactly 1' ); }); QUnit.test('LogarithmicVolumeTransfer is invertible', function(assert) { const transfer = new LogarithmicVolumeTransfer(50); + const tolerance = 0.001; [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0].forEach(slider => { - const linear = transfer.toLinear(slider); - const back = transfer.toLogarithmic(linear); - - assert.close( - back, - slider, - 0.00001, - `Round trip for ${slider}: ${slider} -> ${linear.toFixed(4)} -> ${back.toFixed(4)}` + const linear = transfer.sliderToVolume(slider); + const back = transfer.volumeToSlider(linear); + const diff = Math.abs(back - slider); + + assert.true( + diff < tolerance, + `Round trip for ${slider}: ${slider} -> ${linear.toFixed(4)} -> ${back.toFixed(4)} (diff: ${diff.toExponential(2)})` ); }); }); -QUnit.test('LogarithmicVolumeTransfer provides finer control at low volumes', function(assert) { - const transfer = new LogarithmicVolumeTransfer(50); - - const linear50 = transfer.toLinear(0.5); - - assert.ok( - linear50 < 0.1, - `Slider at 50% gives linear < 10%: ${(linear50 * 100).toFixed(2)}%` - ); - - const linear25 = transfer.toLinear(0.25); - - assert.ok( - linear25 < 0.01, - `Slider at 25% gives linear < 1%: ${(linear25 * 100).toFixed(2)}%` - ); - - const delta1 = transfer.toLinear(0.1) - transfer.toLinear(0); - const delta2 = transfer.toLinear(0.2) - transfer.toLinear(0.1); - - assert.ok( - delta1 < 0.01 && delta2 < 0.01, - 'Small slider movements at low volumes give small linear changes' - ); -}); - QUnit.test('LogarithmicVolumeTransfer with different dbRanges', function(assert) { const transfer40 = new LogarithmicVolumeTransfer(40); const transfer50 = new LogarithmicVolumeTransfer(50); const transfer60 = new LogarithmicVolumeTransfer(60); - const linear40 = transfer40.toLinear(0.5); - const linear50 = transfer50.toLinear(0.5); - const linear60 = transfer60.toLinear(0.5); + const linear40 = transfer40.sliderToVolume(0.5); + const linear50 = transfer50.sliderToVolume(0.5); + const linear60 = transfer60.sliderToVolume(0.5); assert.ok( linear40 > linear50 && linear50 > linear60, @@ -165,25 +120,25 @@ QUnit.test('LogarithmicVolumeTransfer handles edge cases', function(assert) { const transfer = new LogarithmicVolumeTransfer(50); assert.strictEqual( - transfer.toLinear(-0.1), + transfer.sliderToVolume(-0.1), 0, 'Negative slider values return 0' ); assert.strictEqual( - transfer.toLinear(1.1), + transfer.sliderToVolume(1.1), 1, 'Slider values > 1 return 1' ); assert.strictEqual( - transfer.toLogarithmic(-0.1), + transfer.volumeToSlider(-0.1), 0, 'Negative linear values return 0' ); assert.strictEqual( - transfer.toLogarithmic(1.1), + transfer.volumeToSlider(1.1), 1, 'Linear values > 1 return 1' ); From 9a8063f3a34a18787fd828ab5b792d906a5d4ff9 Mon Sep 17 00:00:00 2001 From: Chris Triantafilis Date: Wed, 19 Nov 2025 15:16:56 -0500 Subject: [PATCH 4/5] Add tests for volume-bar to controls.test.js --- test/unit/controls.test.js | 255 +++++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) diff --git a/test/unit/controls.test.js b/test/unit/controls.test.js index e11d5409de..e30c33d054 100644 --- a/test/unit/controls.test.js +++ b/test/unit/controls.test.js @@ -791,3 +791,258 @@ QUnit.module('SmartTV UI Updates (Progress Bar & Time Display)', function(hooks) userSeekSpy.restore(); }); }); + +QUnit.test('VolumeBar initializes with LinearVolumeTransfer by default', function(assert) { + const player = TestHelpers.makePlayer(); + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + assert.ok(volumeBar.volumeTransfer_, 'volumeTransfer_ should be initialized'); + assert.equal( + volumeBar.volumeTransfer_.constructor.name, 'LinearVolumeTransfer', + 'should use LinearVolumeTransfer by default' + ); + + player.dispose(); +}); + +QUnit.test('VolumeBar initializes with LogarithmicVolumeTransfer when logarithmicVolume is true', function(assert) { + const player = TestHelpers.makePlayer({ + logarithmicVolume: true + }); + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + assert.ok(volumeBar.volumeTransfer_, 'volumeTransfer_ should be initialized'); + assert.equal( + volumeBar.volumeTransfer_.constructor.name, 'LogarithmicVolumeTransfer', + 'should use LogarithmicVolumeTransfer when logarithmicVolume is true' + ); + + player.dispose(); +}); + +QUnit.test('VolumeBar getPercent() uses volume transfer function with logarithmic mode', function(assert) { + const player = TestHelpers.makePlayer({ + logarithmicVolume: true + }); + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + player.volume(0); + assert.equal(volumeBar.getPercent(), 0, 'should return 0 for volume 0'); + + player.volume(1); + assert.equal(volumeBar.getPercent(), 1, 'should return 1 for volume 1'); + + player.volume(0.5); + const percent = volumeBar.getPercent(); + + assert.ok(percent > 0.5 && percent < 1, 'should return non-linear value for volume 0.5'); + + player.dispose(); +}); + +QUnit.test('VolumeBar handleMouseMove uses volume transfer with logarithmic mode', function(assert) { + const player = TestHelpers.makePlayer({ + logarithmicVolume: true + }); + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + const originalCalc = volumeBar.calculateDistance; + + volumeBar.calculateDistance = function() { + return 0.5; + }; + + volumeBar.handleMouseMove({ pageX: 100, pageY: 100 }); + + const volume = player.volume(); + + assert.ok(volume > 0 && volume < 0.5, 'logarithmic mode should set low volume for 50% position'); + + volumeBar.calculateDistance = originalCalc; + player.dispose(); +}); + +QUnit.test('VolumeBar stepForward increases volume with logarithmic transfer', function(assert) { + const player = TestHelpers.makePlayer({ + logarithmicVolume: true + }); + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + player.volume(0.5); + const initialVolume = player.volume(); + + volumeBar.stepForward(); + + assert.ok(player.volume() > initialVolume, 'should increase volume'); + assert.ok(player.volume() <= 1, 'should not exceed max volume'); + + player.dispose(); +}); + +QUnit.test('VolumeBar stepBack decreases volume with logarithmic transfer', function(assert) { + const player = TestHelpers.makePlayer({ + logarithmicVolume: true + }); + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + player.volume(0.5); + const initialVolume = player.volume(); + + volumeBar.stepBack(); + + assert.ok(player.volume() < initialVolume, 'should decrease volume'); + assert.ok(player.volume() >= 0, 'should not go below min volume'); + + player.dispose(); +}); + +QUnit.test('VolumeBar passes logarithmicVolumeRange option to LogarithmicVolumeTransfer', function(assert) { + const customRange = 60; + const player = TestHelpers.makePlayer({ + logarithmicVolume: true, + logarithmicVolumeRange: customRange + }); + + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + assert.equal( + volumeBar.volumeTransfer_.dbRange, customRange, + 'should use custom logarithmicVolumeRange value' + ); + + player.dispose(); +}); + +QUnit.test('VolumeBar getPercent() returns correct values with linear transfer', function(assert) { + const player = TestHelpers.makePlayer(); + + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + player.volume(0); + assert.equal(volumeBar.getPercent(), 0, 'should return 0 for volume 0'); + + player.volume(0.5); + assert.equal(volumeBar.getPercent(), 0.5, 'should return 0.5 for volume 0.5'); + + player.volume(1); + assert.equal(volumeBar.getPercent(), 1, 'should return 1 for volume 1'); + + player.dispose(); +}); + +QUnit.test('VolumeBar handleMouseMove() sets correct volume with linear transfer', function(assert) { + const player = TestHelpers.makePlayer(); + + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + const originalCalculateDistance = volumeBar.calculateDistance; + + volumeBar.calculateDistance = function() { + return 0.5; + }; + + const event = { + pageX: 100, + pageY: 100 + }; + + volumeBar.handleMouseMove(event); + + assert.equal(player.volume(), 0.5, 'should set volume to 0.5 for 50% position with linear transfer'); + + volumeBar.calculateDistance = originalCalculateDistance; + player.dispose(); +}); + +QUnit.test('VolumeBar handleMouseMove() sets correct volume with logarithmic transfer', function(assert) { + const player = TestHelpers.makePlayer({ + logarithmicVolume: true + }); + + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + const originalCalculateDistance = volumeBar.calculateDistance; + + volumeBar.calculateDistance = function() { + return 0.5; + }; + + const event = { + pageX: 100, + pageY: 100 + }; + + volumeBar.handleMouseMove(event); + + const volume = player.volume(); + + assert.ok(volume < 0.5, 'logarithmic transfer should set volume < 0.5 for 50% slider position'); + assert.ok(volume > 0, 'volume should be greater than 0'); + + volumeBar.calculateDistance = originalCalculateDistance; + player.dispose(); +}); + +QUnit.test('VolumeBar stepForward() increases volume correctly with linear transfer', function(assert) { + const player = TestHelpers.makePlayer(); + + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + player.volume(0.5); + volumeBar.stepForward(); + + assert.ok(player.volume() > 0.5, 'should increase volume'); + assert.ok(player.volume() <= 1, 'should not exceed max volume'); + + player.dispose(); +}); + +QUnit.test('VolumeBar stepForward() increases volume correctly with logarithmic transfer', function(assert) { + const player = TestHelpers.makePlayer({ + logarithmicVolume: true + }); + + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + const initialVolume = 0.5; + + player.volume(initialVolume); + volumeBar.stepForward(); + + assert.ok(player.volume() > initialVolume, 'should increase volume'); + assert.ok(player.volume() <= 1, 'should not exceed max volume'); + + player.dispose(); +}); + +QUnit.test('VolumeBar stepBack() decreases volume correctly with linear transfer', function(assert) { + const player = TestHelpers.makePlayer(); + + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + player.volume(0.5); + volumeBar.stepBack(); + + assert.ok(player.volume() < 0.5, 'should decrease volume'); + assert.ok(player.volume() >= 0, 'should not go below min volume'); + + player.dispose(); +}); + +QUnit.test('VolumeBar stepBack() decreases volume correctly with logarithmic transfer', function(assert) { + const player = TestHelpers.makePlayer({ + logarithmicVolume: true + }); + + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + const initialVolume = 0.5; + + player.volume(initialVolume); + volumeBar.stepBack(); + + assert.ok(player.volume() < initialVolume, 'should decrease volume'); + assert.ok(player.volume() >= 0, 'should not go below min volume'); + + player.dispose(); +}); From 3a8ff94085f2a33b20be2bd60f5679d9012a9d88 Mon Sep 17 00:00:00 2001 From: Chris Triantafilis Date: Sat, 6 Dec 2025 18:29:35 -0500 Subject: [PATCH 5/5] Add one more test for codecov --- test/unit/controls.test.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/unit/controls.test.js b/test/unit/controls.test.js index e30c33d054..ab840c2d2e 100644 --- a/test/unit/controls.test.js +++ b/test/unit/controls.test.js @@ -1046,3 +1046,17 @@ QUnit.test('VolumeBar stepBack() decreases volume correctly with logarithmic tra player.dispose(); }); + +QUnit.test('VolumeBar stepBack() sets volume to 0 when slider would go below threshold', function(assert) { + const player = TestHelpers.makePlayer(); + + const volumeBar = player.controlBar.volumePanel.volumeControl.volumeBar; + + player.volume(0.02); + + volumeBar.stepBack(); + + assert.equal(player.volume(), 0, 'volume is set to 0 when reduced slider is below threshold'); + + player.dispose(); +});