Add volume slider to the transport bar
A slim horizontal master-volume slider in its own slot between the visualizer and the timeline (TASKS.md). Perceptual->linear gain so it ramps like iTunes; persists to preferences.json (debounced so it doesn't thrash the theme). Clicking jumps to the spot via a new ClickJumpSlider base extracted from SeekSlider.
This commit is contained in:
@@ -49,6 +49,14 @@ QFrame#nowPlayingBox {
|
||||
TITLE_PT_BUMP = 2
|
||||
|
||||
|
||||
def _clamp_volume(value) -> float:
|
||||
"""Coerce a stored preference to a logical 0..1 volume."""
|
||||
try:
|
||||
return max(0.0, min(1.0, float(value)))
|
||||
except (TypeError, ValueError):
|
||||
return 0.8
|
||||
|
||||
|
||||
class FlashButton(QToolButton):
|
||||
"""Transport button: dark-gray glyph that shows the highlight color
|
||||
while pressed (a quick flash on click, sustained while held). A checkable
|
||||
@@ -101,9 +109,10 @@ def _box(*widgets, margins=(10, 3, 10, 3), hug=False, height=None) -> QFrame:
|
||||
return frame
|
||||
|
||||
|
||||
class SeekSlider(QSlider):
|
||||
"""Timeline slider: clicking anywhere jumps the handle (and playhead)
|
||||
straight to the clicked spot instead of paging; dragging scrubs."""
|
||||
class ClickJumpSlider(QSlider):
|
||||
"""Horizontal slider where clicking anywhere jumps the handle straight to
|
||||
the clicked spot (instead of paging one step toward it); dragging tracks
|
||||
the cursor. Shared by the timeline and the volume slider."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(Qt.Orientation.Horizontal, parent)
|
||||
@@ -113,7 +122,8 @@ class SeekSlider(QSlider):
|
||||
self.minimum(), self.maximum(), int(x), self.width())
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton and self.maximum() > 0:
|
||||
if (event.button() == Qt.MouseButton.LeftButton
|
||||
and self.maximum() > self.minimum()):
|
||||
self.setSliderDown(True) # emits sliderPressed
|
||||
self.setSliderPosition(self._value_at(event.position().x()))
|
||||
event.accept()
|
||||
@@ -135,6 +145,11 @@ class SeekSlider(QSlider):
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class SeekSlider(ClickJumpSlider):
|
||||
"""Timeline slider: clicking anywhere jumps the handle (and playhead)
|
||||
straight to the clicked spot instead of paging; dragging scrubs."""
|
||||
|
||||
|
||||
class BpmButton(QPushButton):
|
||||
"""Tap-tempo button: tap along to the music; when the cursor leaves,
|
||||
the shown bpm is saved to the playing track 3 seconds later."""
|
||||
@@ -231,6 +246,27 @@ class TransportBar(QWidget):
|
||||
layout.addWidget(self._visualizer)
|
||||
layout.addSpacing(10)
|
||||
|
||||
# Master output volume: a slim horizontal slider in its own slot
|
||||
# between the visualizer and the timeline. The HBox centers it
|
||||
# vertically, so nothing stacks above or below it (iTunes-style).
|
||||
self._volume_slider = ClickJumpSlider()
|
||||
self._volume_slider.setRange(0, 100)
|
||||
self._volume_slider.setFixedWidth(96)
|
||||
self._volume_slider.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
self._volume_slider.setToolTip("Volume")
|
||||
initial_volume = _clamp_volume(self._prefs.get("volume", 0.8))
|
||||
self._volume_slider.setValue(round(initial_volume * 100))
|
||||
player.set_volume(initial_volume)
|
||||
# Persisting volume re-applies the theme (Preferences.changed), so save
|
||||
# only after the user stops sliding — audio still tracks every tick.
|
||||
self._volume_save_timer = QTimer(self)
|
||||
self._volume_save_timer.setSingleShot(True)
|
||||
self._volume_save_timer.setInterval(400)
|
||||
self._volume_save_timer.timeout.connect(self._save_volume)
|
||||
self._volume_slider.valueChanged.connect(self._on_volume_changed)
|
||||
layout.addWidget(self._volume_slider)
|
||||
layout.addSpacing(10)
|
||||
|
||||
center = QVBoxLayout()
|
||||
center.setSpacing(0)
|
||||
|
||||
@@ -302,6 +338,14 @@ class TransportBar(QWidget):
|
||||
button.refresh_icons()
|
||||
self._apply_now_playing_font()
|
||||
|
||||
def _on_volume_changed(self, value: int):
|
||||
"""Slider moved: update audio live, defer the (theme-reapplying) save."""
|
||||
self._player.set_volume(value / 100)
|
||||
self._volume_save_timer.start()
|
||||
|
||||
def _save_volume(self):
|
||||
self._prefs.set("volume", self._volume_slider.value() / 100)
|
||||
|
||||
def _apply_now_playing_font(self):
|
||||
"""Set the now-playing labels to Century Gothic (or the user's chosen
|
||||
substitute), black text; title slightly larger than artist/album.
|
||||
|
||||
+21
-1
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
|
||||
from PyQt6.QtCore import QObject, QUrl, pyqtSignal
|
||||
from PyQt6.QtMultimedia import (
|
||||
QMediaPlayer, QAudioOutput, QAudioBufferOutput, QMediaDevices)
|
||||
QMediaPlayer, QAudioOutput, QAudioBufferOutput, QMediaDevices, QAudio)
|
||||
|
||||
from lintunes.models import Track
|
||||
|
||||
@@ -53,6 +53,7 @@ class Player(QObject):
|
||||
self._counted_finish_id: int | None = None
|
||||
|
||||
self._audio = QAudioOutput(self)
|
||||
self._volume = 1.0 # logical 0..1; what the volume slider shows
|
||||
self._media = QMediaPlayer(self)
|
||||
self._media.setAudioOutput(self._audio)
|
||||
# Tee of the decoded PCM, feeding the visualizer
|
||||
@@ -138,6 +139,25 @@ class Player(QObject):
|
||||
def duration_ms(self) -> int:
|
||||
return self._media.duration()
|
||||
|
||||
# ---- volume ----
|
||||
|
||||
def set_volume(self, level: float):
|
||||
"""Set output volume from a logical 0..1 value (0=silent, 1=full).
|
||||
The slider scale is perceptual, so convert to the linear gain
|
||||
QAudioOutput expects — that makes the knob feel iTunes-like rather
|
||||
than jumping to "loud" in the first few percent."""
|
||||
level = max(0.0, min(1.0, float(level)))
|
||||
self._volume = level
|
||||
self._audio.setVolume(QAudio.convertVolume(
|
||||
level,
|
||||
QAudio.VolumeScale.LogarithmicVolumeScale,
|
||||
QAudio.VolumeScale.LinearVolumeScale,
|
||||
))
|
||||
|
||||
def volume(self) -> float:
|
||||
"""The logical 0..1 volume last set (what the slider shows)."""
|
||||
return self._volume
|
||||
|
||||
# ---- transport ----
|
||||
|
||||
def toggle_play(self):
|
||||
|
||||
@@ -10,6 +10,7 @@ DEFAULTS = {
|
||||
"highlight": "blue", # see theme.HIGHLIGHT_COLORS
|
||||
"now_playing_font": None, # None=prefer Century Gothic, ""=use app
|
||||
# default, "Family"=user-chosen substitute
|
||||
"volume": 0.8, # logical 0..1 master output volume
|
||||
"lastfm": {
|
||||
"api_key": "",
|
||||
"api_secret": "",
|
||||
|
||||
Reference in New Issue
Block a user