diff --git a/backend/app.go b/backend/app.go index 7f59d9e..77a92fc 100644 --- a/backend/app.go +++ b/backend/app.go @@ -9,6 +9,7 @@ import ( "path" "supersonic/backend/util" "supersonic/player" + "supersonic/sharedutil" "github.com/20after4/configdir" "github.com/zalando/go-keyring" @@ -51,6 +52,16 @@ func StartupApp() (*App, error) { a.Config.LocalPlayback.Volume = clamp(a.Config.LocalPlayback.Volume, 0, 100) a.Player.SetVolume(a.Config.LocalPlayback.Volume) + rgainOpts := []string{ReplayGainNone, ReplayGainAlbum, ReplayGainTrack} + if !sharedutil.StringSliceContains(rgainOpts, a.Config.ReplayGain.Mode) { + a.Config.ReplayGain.Mode = ReplayGainNone + } + a.Player.SetReplayGainOptions(player.ReplayGainOptions{ + Mode: player.ReplayGainMode(a.Config.ReplayGain.Mode), + PreventClipping: a.Config.ReplayGain.PreventClipping, + PreampGain: a.Config.ReplayGain.PreampGainDB, + }) + a.ServerManager = NewServerManager() a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player, &a.Config.Scrobbling) a.LibraryManager = NewLibraryManager(a.ServerManager) diff --git a/backend/config.go b/backend/config.go index 8fddff0..da2455f 100644 --- a/backend/config.go +++ b/backend/config.go @@ -59,6 +59,12 @@ type ScrobbleConfig struct { ThresholdPercent int } +type ReplayGainConfig struct { + Mode string + PreampGainDB float64 + PreventClipping bool +} + type Config struct { Application AppConfig Servers []*ServerConfig @@ -70,6 +76,7 @@ type Config struct { PlaylistPage PlaylistPageConfig LocalPlayback LocalPlaybackConfig Scrobbling ScrobbleConfig + ReplayGain ReplayGainConfig } func DefaultConfig() *Config { @@ -108,6 +115,11 @@ func DefaultConfig() *Config { ThresholdTimeSeconds: 240, ThresholdPercent: 50, }, + ReplayGain: ReplayGainConfig{ + Mode: ReplayGainNone, + PreampGainDB: 0.0, + PreventClipping: true, + }, } } diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 445ddbe..1db3131 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -12,6 +12,12 @@ import ( "github.com/dweymouth/go-subsonic/subsonic" ) +const ( + ReplayGainNone = string(player.ReplayGainNone) + ReplayGainAlbum = string(player.ReplayGainAlbum) + ReplayGainTrack = string(player.ReplayGainTrack) +) + type PlaybackManager struct { ctx context.Context cancelPollPos context.CancelFunc diff --git a/player/libmpv.go b/player/libmpv.go index 52e4cde..6ccb973 100644 --- a/player/libmpv.go +++ b/player/libmpv.go @@ -104,6 +104,14 @@ func (m libmpv) SetProperty(name string, format MPVFormat, value any) error { return toMPVError(C.mpv_set_property(m.handle, cname, C.mpv_format(format), p)) } +func (m libmpv) SetPropertyString(name, value string) error { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + cvalue := C.CString(value) + defer C.free(unsafe.Pointer(cvalue)) + return toMPVError(C.mpv_set_property_string(m.handle, cname, cvalue)) +} + func (m libmpv) GetProperty(name string, format MPVFormat) (any, error) { cname := C.CString(name) defer C.free(unsafe.Pointer(cname)) diff --git a/player/player.go b/player/player.go index 095dd40..15ee9e6 100644 --- a/player/player.go +++ b/player/player.go @@ -38,12 +38,31 @@ const ( SeekRelativePercent ) +// One of "no", "track", or "album" +type ReplayGainMode string + +const ( + ReplayGainNone ReplayGainMode = "no" + ReplayGainTrack ReplayGainMode = "track" + ReplayGainAlbum ReplayGainMode = "album" +) + +// Replay Gain options (argument to SetReplayGainOptions). +type ReplayGainOptions struct { + Mode ReplayGainMode + PreampGain float64 + PreventClipping bool + // Fallback gain intentionally omitted +} + // Player encapsulates the mpv instance and provides functions // to control it and to check its status. type Player struct { mpv libmpv initialized bool vol int + replayGainOpts ReplayGainOptions + haveRGainOpts bool status Status seeking bool curPlaylistPos int64 @@ -102,6 +121,9 @@ func (p *Player) Init(audioExclusive bool, maxCacheMB int) error { if audioExclusive { m.SetOptionString("audio-exclusive", "yes") } + if p.haveRGainOpts { + p.SetReplayGainOptions(p.replayGainOpts) + } if p.clientName != "" { m.SetOptionString("audio-client-name", p.clientName) @@ -235,6 +257,30 @@ func (p *Player) SetVolume(vol int) error { return nil } +// Sets the ReplayGain options of the player. +// Unlike most Player functions, SetReplayGainOptions can be called +// before Init, to set the initial volume of the player on startup. +func (p *Player) SetReplayGainOptions(options ReplayGainOptions) error { + p.replayGainOpts = options + p.haveRGainOpts = true + if p.initialized { + if err := p.mpv.SetPropertyString("replaygain", string(options.Mode)); err != nil { + return err + } + if err := p.mpv.SetProperty("replaygain-preamp", MPVFormatDouble, options.PreampGain); err != nil { + return err + } + clip := "no" + if options.PreventClipping { + clip = "yes" + } + if err := p.mpv.SetPropertyString("replaygain-clip", clip); err != nil { + return err + } + } + return nil +} + // Gets the current volume of the player. func (p *Player) GetVolume() int { return p.vol