Add playback speed control (0.25x - 4x) with UI selector#766
Add playback speed control (0.25x - 4x) with UI selector#766localhost5173 wants to merge 2 commits intodweymouth:mainfrom
Conversation
| SetVolume(int) error | ||
| GetVolume() int | ||
|
|
||
| SetSpeed(float64) error |
There was a problem hiding this comment.
Rather than having these on the BasePlayer interface I think it makes sense to have a SpeedPlayer interface that has just these two functions. Only the MPV player would implement it, and then the UI code (either in bottompanel.go or maybe mainwindow.go) can subscribe to the PlaybackManager.OnPlayerChange hook to interface-assert against the SpeedPlayer interface and disable the UI button if the current player does not support changing speed.
| return p.engine.CurrentPlayer().GetVolume() | ||
| } | ||
|
|
||
| func (p *PlaybackManager) SetSpeed(speed float64) { |
There was a problem hiding this comment.
Would you be able to look into implementing the MPRIS support for this as well? There are Rate and SetRate stubs on the MPRIS handler here - https://github.com/dweymouth/supersonic/blob/main/backend/mpris.go#L242. Exporting Rate properly via MPRIS will keep the OS's concept of the playback position in sync with the actual position.
It would also be good to implement the same for Windows and Mac, though if you prefer, I can merge this PR to a feature branch once it's ready and then finish that myself. If nothing else, it can just give the OS an updated playback position once every 5 or 10 seconds to keep it from getting too out-of-sync.
| } | ||
|
|
||
| func (a *AuxControls) showSpeedMenu() { | ||
| speeds := []float64{0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75, 4.0} |
There was a problem hiding this comment.
I would prefer instead of a huge menu like this if the menu had fewer options (e.g. 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2, 4) plus a "Custom" option that would launch a PopUp with a slider to set a custom speed outside these options. That is something else I can do in a follow-up if you prefer.
There was a problem hiding this comment.
FWIW i'm following this issue as I use speed changes for many reasons, speedup slow podcasts, slow down music for learning on guitar, changing the pitch to see if it blends with other songs etc. For a non dj music app i think Symphonium has a nice interface that strikes a balance between useful presets and customization:

Not trying to derail or influence really, just provide a nice example interface from another personal streaming app I use. ❤️
…playback speed changes
There was a problem hiding this comment.
Pull request overview
Adds playback speed control to Supersonic (0.25x–4.0x) by wiring a new UI selector through PlaybackManager down into the underlying player implementations.
Changes:
- Add a new playback-speed selector button/menu to the bottom “aux controls” UI.
- Extend the backend player interface + playback command queue/engine/manager to support setting/getting speed.
- Implement speed support for the MPV player, and stub implementations for DLNA/Jukebox players.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/widgets/auxcontrols.go | Adds a speed button and popup menu to select playback speed; highlights when non-1.0x. |
| ui/bottompanel.go | Wires AuxControls speed changes into PlaybackManager and initializes the speed indicator. |
| backend/player/player.go | Extends BasePlayer interface with SetSpeed/GetSpeed. |
| backend/player/mpv/player.go | Implements MPV speed set/get via the speed property. |
| backend/player/jukebox/jukeboxplayer.go | Adds no-op speed methods for jukebox mode. |
| backend/player/dlna/dlnaplayer.go | Adds no-op speed methods for DLNA mode. |
| backend/playbackmanager.go | Adds SetSpeed/Speed and handles cmdSpeed in the command loop. |
| backend/playbackengine.go | Adds speed clamping + forwards to current player. |
| backend/playbackcommands.go | Adds a cmdSpeed command and queue method for coalescing speed changes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| bp.AuxControls.OnChangeSpeed = func(speed float64) { | ||
| pm.SetSpeed(speed) | ||
| } | ||
| // Set initial speed from playback manager | ||
| bp.AuxControls.SetSpeed(pm.Speed()) |
There was a problem hiding this comment.
AuxControls speed state is only initialized once via SetSpeed(pm.Speed()). If the user switches between local/remote players (pm.OnPlayerChange), the speed indicator can become stale (e.g., remain highlighted when casting to a player that always reports 1.0x). Consider updating the existing OnPlayerChange handler to also sync bp.AuxControls.SetSpeed(pm.Speed()) (and/or disable the speed control when not supported).
| // SetSpeed is not supported for Jukebox players, returns nil (no-op). | ||
| func (j *JukeboxPlayer) SetSpeed(speed float64) error { | ||
| // Jukebox mode does not support playback speed control | ||
| return nil | ||
| } |
There was a problem hiding this comment.
SetSpeed() is implemented as a silent no-op returning nil, but GetSpeed() always returns 1.0. That makes it impossible for callers to distinguish “speed set successfully” from “unsupported”, and can lead the UI to show a non-1.0x state even though playback speed can’t change. Prefer returning a non-nil error for unsupported speed control (or otherwise reflect the requested speed consistently).
| // SetSpeed is not supported for DLNA players, returns nil (no-op). | ||
| func (d *DLNAPlayer) SetSpeed(speed float64) error { | ||
| // DLNA/UPnP does not typically support playback speed control | ||
| return nil |
There was a problem hiding this comment.
SetSpeed() is a silent no-op returning nil while GetSpeed() always returns 1.0. This makes the operation appear successful to callers even though playback speed cannot change, which can desync UI state. Prefer returning an explicit error for unsupported speed control (or otherwise expose support/actual speed to callers).
| // SetSpeed is not supported for DLNA players, returns nil (no-op). | |
| func (d *DLNAPlayer) SetSpeed(speed float64) error { | |
| // DLNA/UPnP does not typically support playback speed control | |
| return nil | |
| // SetSpeed is not supported for DLNA players; return an explicit error instead of silently no-op'ing. | |
| func (d *DLNAPlayer) SetSpeed(speed float64) error { | |
| if d.destroyed { | |
| // Maintain existing behavior for destroyed players. | |
| return nil | |
| } | |
| // DLNA/UPnP does not typically support playback speed control. | |
| return errors.New("DLNA playback speed control is not supported") |
| } | ||
|
|
||
| a.speed.IconSize = IconButtonSizeSmaller | ||
| a.speed.SetToolTip("Playback speed") |
There was a problem hiding this comment.
New UI strings for the speed control aren’t localized (e.g., tooltip text). The rest of this widget uses lang.L(...) for user-visible strings, so these should also go through the localization layer for consistency and translation support.
| a.speed.SetToolTip("Playback speed") | |
| a.speed.SetToolTip(lang.L("Playback speed")) |
| label := fmt.Sprintf("%.2fx", s) | ||
| if s == 1.0 { | ||
| label = "1x (Normal)" |
There was a problem hiding this comment.
The speed menu item label "1x (Normal)" (and the general formatting of speed labels) is hard-coded English text. Please route these labels through lang.L(...) (and format using the localized template) so translations can cover the speed selector.
| label := fmt.Sprintf("%.2fx", s) | |
| if s == 1.0 { | |
| label = "1x (Normal)" | |
| label := lang.L("%.2fx", s) | |
| if s == 1.0 { | |
| label = lang.L("1x (Normal)") |
This PR adds playback speed control functionality to Supersonic, allowing users to adjust playback speed from 0.25x to 4.0x in 0.25x increments.