diff --git a/backend/app.go b/backend/app.go index ebc3450..539ebc8 100644 --- a/backend/app.go +++ b/backend/app.go @@ -12,6 +12,7 @@ import ( "github.com/dweymouth/supersonic/backend/util" "github.com/dweymouth/supersonic/player" + "github.com/dweymouth/supersonic/player/mpv" "github.com/dweymouth/supersonic/sharedutil" "github.com/fsnotify/fsnotify" "github.com/google/uuid" @@ -36,7 +37,7 @@ type App struct { ServerManager *ServerManager ImageManager *ImageManager PlaybackManager *PlaybackManager - Player *player.Player + Player *mpv.Player UpdateChecker UpdateChecker MPRISHandler *MPRISHandler @@ -194,7 +195,7 @@ func (a *App) callOnReactivate() { } func (a *App) initMPV() error { - p := player.NewWithClientName(a.appName) + p := mpv.NewWithClientName(a.appName) c := a.Config.LocalPlayback c.InMemoryCacheSizeMB = clamp(c.InMemoryCacheSizeMB, 10, 500) if err := p.Init(c.InMemoryCacheSizeMB); err != nil { @@ -234,10 +235,16 @@ func (a *App) setupMPV() error { if !sharedutil.SliceContains(rgainOpts, a.Config.ReplayGain.Mode) { a.Config.ReplayGain.Mode = ReplayGainNone } - mode := player.ReplayGainMode(a.Config.ReplayGain.Mode) - if a.Config.ReplayGain.Mode == ReplayGainAuto { + mode := player.ReplayGainNone + switch a.Config.ReplayGain.Mode { + case ReplayGainAlbum: + mode = player.ReplayGainAlbum + case ReplayGainTrack: + mode = player.ReplayGainTrack + case ReplayGainAuto: mode = player.ReplayGainTrack } + a.Player.SetReplayGainOptions(player.ReplayGainOptions{ Mode: mode, PreventClipping: a.Config.ReplayGain.PreventClipping, @@ -245,7 +252,7 @@ func (a *App) setupMPV() error { }) a.Player.SetAudioExclusive(a.Config.LocalPlayback.AudioExclusive) - eq := &player.ISO15BandEqualizer{ + eq := &mpv.ISO15BandEqualizer{ EQPreamp: a.Config.LocalPlayback.EqualizerPreamp, Disabled: !a.Config.LocalPlayback.EqualizerEnabled, } diff --git a/backend/mpmedia_mac.go b/backend/mpmedia_mac.go index 2ea3308..6abed67 100644 --- a/backend/mpmedia_mac.go +++ b/backend/mpmedia_mac.go @@ -23,6 +23,7 @@ import ( "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/player" + "github.com/dweymouth/supersonic/player/mpv" ) // os_remote_command_callback is called by Objective-C when incoming OS media commands are received. @@ -51,7 +52,7 @@ func os_remote_command_callback(command C.Command, value C.double) { // MPMediaHandler is the handler for MacOS media controls and system events. type MPMediaHandler struct { - player *player.Player + player *mpv.Player playbackManager *PlaybackManager artURLLookup func(string) (string, error) } @@ -62,7 +63,7 @@ var mpMediaEventRecipient *MPMediaHandler // NewMPMediaHandler creates a new MPMediaHandler instances and sets it as the current recipient // for incoming system events. -func InitMPMediaHandler(player *player.Player, playbackManager *PlaybackManager, artURLLookup func(trackID string) (string, error)) error { +func InitMPMediaHandler(player *mpv.Player, playbackManager *PlaybackManager, artURLLookup func(trackID string) (string, error)) error { mp := &MPMediaHandler{ player: player, playbackManager: playbackManager, @@ -187,5 +188,5 @@ func (mp *MPMediaHandler) OnCommandSeek(positionSeconds float64) { if mp == nil || mp.player == nil { return } - mp.player.Seek(fmt.Sprintf("%0.2f", positionSeconds), player.SeekAbsolute) + mp.player.Seek(fmt.Sprintf("%0.2f", positionSeconds), mpv.SeekAbsolute) } diff --git a/backend/mpris.go b/backend/mpris.go index d1b6b3c..a425403 100644 --- a/backend/mpris.go +++ b/backend/mpris.go @@ -8,6 +8,7 @@ import ( "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/player" + "github.com/dweymouth/supersonic/player/mpv" "github.com/godbus/dbus/v5" "github.com/quarckster/go-mpris-server/pkg/events" "github.com/quarckster/go-mpris-server/pkg/server" @@ -43,13 +44,13 @@ type MPRISHandler struct { connErr error playerName string curTrackPath string // empty for no track - p *player.Player + p *mpv.Player pm *PlaybackManager s *server.Server evt *events.EventHandler } -func NewMPRISHandler(playerName string, p *player.Player, pm *PlaybackManager) *MPRISHandler { +func NewMPRISHandler(playerName string, p *mpv.Player, pm *PlaybackManager) *MPRISHandler { m := &MPRISHandler{playerName: playerName, p: p, pm: pm, connErr: errors.New("not started")} m.s = server.NewServer(playerName, m, m) m.evt = events.NewEventHandler(m.s) @@ -180,12 +181,12 @@ func (m *MPRISHandler) Play() error { } func (m *MPRISHandler) Seek(offset types.Microseconds) error { - return m.p.Seek(fmt.Sprintf("%0.2f", microsecondsToSeconds(offset)), player.SeekRelative) + return m.p.Seek(fmt.Sprintf("%0.2f", microsecondsToSeconds(offset)), mpv.SeekRelative) } func (m *MPRISHandler) SetPosition(trackId string, position types.Microseconds) error { if m.curTrackPath == trackId { - return m.p.Seek(fmt.Sprintf("%0.2f", microsecondsToSeconds(position)), player.SeekAbsolute) + return m.p.Seek(fmt.Sprintf("%0.2f", microsecondsToSeconds(position)), mpv.SeekAbsolute) } return nil } @@ -326,4 +327,3 @@ func encodeTrackId(id string) string { data := []byte(id) return base32.StdEncoding.WithPadding('0').EncodeToString(data) } - diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index b3af65d..5be630a 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -8,13 +8,14 @@ import ( "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/util" "github.com/dweymouth/supersonic/player" + "github.com/dweymouth/supersonic/player/mpv" "github.com/dweymouth/supersonic/sharedutil" ) -const ( - ReplayGainNone = string(player.ReplayGainNone) - ReplayGainAlbum = string(player.ReplayGainAlbum) - ReplayGainTrack = string(player.ReplayGainTrack) +var ( + ReplayGainNone = player.ReplayGainNone.String() + ReplayGainAlbum = player.ReplayGainAlbum.String() + ReplayGainTrack = player.ReplayGainTrack.String() ReplayGainAuto = "Auto" ) @@ -34,7 +35,7 @@ type PlaybackManager struct { cancelPollPos context.CancelFunc pollingTick *time.Ticker sm *ServerManager - player *player.Player + player *mpv.Player playTimeStopwatch util.Stopwatch curTrackTime float64 @@ -59,7 +60,7 @@ type PlaybackManager struct { func NewPlaybackManager( ctx context.Context, s *ServerManager, - p *player.Player, + p *mpv.Player, scrobbleCfg *ScrobbleConfig, transcodeCfg *TranscodingConfig, ) *PlaybackManager { @@ -335,7 +336,16 @@ func (p *PlaybackManager) StopAndClearPlayQueue() { func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) { p.replayGainCfg = config - mode := player.ReplayGainMode(config.Mode) + mode := player.ReplayGainNone + switch config.Mode { + case ReplayGainAuto: + mode = player.ReplayGainTrack + case ReplayGainTrack: + mode = player.ReplayGainTrack + case ReplayGainAlbum: + mode = player.ReplayGainAlbum + } + if config.Mode == ReplayGainAuto { mode = player.ReplayGainTrack } diff --git a/player/equalizer.go b/player/mpv/equalizer.go similarity index 99% rename from player/equalizer.go rename to player/mpv/equalizer.go index 1e81e5d..e3a1eaa 100644 --- a/player/equalizer.go +++ b/player/mpv/equalizer.go @@ -1,4 +1,4 @@ -package player +package mpv // Equalizer implementations based on the ffmpeg 'equalizer' filter diff --git a/player/mpv/player.go b/player/mpv/player.go new file mode 100644 index 0000000..afad809 --- /dev/null +++ b/player/mpv/player.go @@ -0,0 +1,693 @@ +package mpv + +import ( + "context" + "errors" + "fmt" + "log" + "math" + "strconv" + + "github.com/dweymouth/go-mpv" + "github.com/dweymouth/supersonic/player" +) + +// Error returned by many Player functions if called before the player has not been initialized. +var ErrUnitialized error = errors.New("mpv player uninitialized") + +// Argument to Seek function (SeekAbsolute, SeekRelative, SeekAbsolutePercent, SeekRelativePercent). +type SeekMode int + +const ( + SeekAbsolute SeekMode = iota + SeekRelative + SeekAbsolutePercent + SeekRelativePercent +) + +// One of "no", "track", or "album" +type ReplayGainMode string + +const ( + ReplayGainNone ReplayGainMode = "no" + ReplayGainTrack ReplayGainMode = "track" + ReplayGainAlbum ReplayGainMode = "album" +) + +// Information about a specific audio device. +// Returned by ListAudioDevices. +type AudioDevice struct { + // The name of the audio device. + // This is the string to pass to SetAudioDevice. + Name string + + // The description of the audio device. + // This is the friendly string that should be used in UIs. + Description string +} + +// Media information about the currently playing media. +type MediaInfo struct { + // The sample format as string. This uses the same names as used in other places of mpv. + // NOTE: this is the format that the decoder outputs, NOT necessarily the format of the file. + Format string + + // Audio samplerate. + Samplerate int + + // The number of channels. + ChannelCount int + + // The audio codec. + Codec string + + // The average bit rate in bits per second. + Bitrate int +} + +// Player encapsulates the mpv instance and provides functions +// to control it and to check its status. +type Player struct { + mpv *mpv.Mpv + initialized bool + vol int + replayGainOpts player.ReplayGainOptions + haveRGainOpts bool + audioExclusive bool + status player.Status + loopMode player.LoopMode + seeking bool + curPlaylistPos int64 + prePausedState player.State + clientName string + equalizer Equalizer + + bgCancel context.CancelFunc + + // callbacks + onPaused []func() + onStopped []func() + onPlaying []func() + onSeek []func() + onTrackChange []func(int64) +} + +// Returns a new player. +// Must call Init on the player before it is ready for playback. +func New() *Player { + return NewWithClientName("") +} + +// Same as New, but sets the application name that mpv +// reports to the system audio API. +func NewWithClientName(c string) *Player { + return &Player{ + vol: -1, // use 100 in Init + clientName: c, + } +} + +// Initializes the Player and makes it ready for playback. +// Most Player functions will return ErrUnitialized if called before Init. +func (p *Player) Init(maxCacheMB int) error { + if !p.initialized { + m := mpv.Create() + + m.SetOptionString("idle", "yes") + m.SetOptionString("video", "no") + m.SetOptionString("audio-display", "no") + m.SetOptionString("gapless-audio", "weak") + m.SetOptionString("prefetch-playlist", "yes") + m.SetOptionString("force-seekable", "yes") + m.SetOptionString("terminal", "no") + + // limit in-memory cache size + m.SetOptionString("demuxer-max-bytes", fmt.Sprintf("%dMiB", maxCacheMB)) + + if p.vol < 0 { + p.vol = 100 + } + m.SetOption("volume", mpv.FORMAT_INT64, p.vol) + + p.SetAudioExclusive(p.audioExclusive) + if p.haveRGainOpts { + p.SetReplayGainOptions(p.replayGainOpts) + } + + if p.clientName != "" { + m.SetOptionString("audio-client-name", p.clientName) + } + + if err := m.Initialize(); err != nil { + return fmt.Errorf("error initializing mpv: %s", err.Error()) + } + p.mpv = m + } + ctx, cancel := context.WithCancel(context.Background()) + go p.eventHandler(ctx) + p.bgCancel = cancel + p.initialized = true + return nil +} + +// Appends the given file to the play queue. +// Note that the Player API does not provide methods to read +// the play queue. Clients are expected to maintain their own play queue model. +func (p *Player) AppendFile(url string) error { + log.Printf("Adding playback URL: %s", url) + if !p.initialized { + return ErrUnitialized + } + return p.mpv.Command([]string{"loadfile", url, "append"}) +} + +// Plays the specified file, clearing the previous play queue, if any. +func (p *Player) PlayFile(url string) error { + log.Printf("Adding playback URL: %s", url) + if !p.initialized { + return ErrUnitialized + } + err := p.mpv.Command([]string{"loadfile", url, "replace"}) + if err == nil { + p.setState(player.Playing) + } + return err +} + +// Removes the item at the given index from the internal playqueue. +func (p *Player) RemoveTrackAt(idx int) error { + if !p.initialized { + return ErrUnitialized + } + return p.mpv.Command([]string{"playlist-remove", strconv.Itoa(idx)}) +} + +// Stops playback and clears the play queue. +func (p *Player) Stop() error { + if !p.initialized { + return ErrUnitialized + } + var err error + if p.status.State == player.Stopped { + err = p.mpv.Command([]string{"playlist-clear"}) + } else { + if err = p.mpv.Command([]string{"stop"}); err == nil { + // if player was paused, stop command actually doesn't clear pause state + err = p.setPaused(false) + } + } + if err == nil { + p.setState(player.Stopped) + } + return err +} + +// Clears the play queue, except for the currently playing file. +func (p *Player) ClearPlayQueue() error { + if !p.initialized { + return ErrUnitialized + } + return p.mpv.Command([]string{"playlist-clear"}) +} + +// Seeks within the currently playing track. +// See MPV seek command documentation for more details. +func (p *Player) Seek(target string, mode SeekMode) error { + if !p.initialized { + return ErrUnitialized + } + p.seeking = true + err := p.mpv.Command([]string{"seek", target, mode.String()}) + return err +} + +// Seeks to the beginning of the current track if: +// - The current track is the first track in the play queue, or +// - The current time is more than 3 seconds past the beginning of the track. +// +// Else seeks to the beginning of the previous track. +func (p *Player) SeekBackOrPrevious() error { + if !p.initialized { + return ErrUnitialized + } + + if pos, err := p.getInt64Property("time-pos"); err == nil && pos > 3 { + return p.Seek("0", SeekAbsolutePercent) + } + if pos, err := p.getInt64Property("playlist-pos"); err == nil && pos == 0 { + return p.Seek("0", SeekAbsolutePercent) + } + return p.mpv.Command([]string{"playlist-prev"}) +} + +// Seeks to the next track in the play queue, if any. +func (p *Player) SeekNext() error { + if !p.initialized { + return ErrUnitialized + } + return p.mpv.Command([]string{"playlist-next"}) +} + +// Sets the volume of the player (0-100). +// Unlike most Player functions, SetVolume can be called before Init, +// to set the initial volume of the player on startup. +func (p *Player) SetVolume(vol int) error { + if vol > 100 { + vol = 100 + } else if vol < 0 { + vol = 0 + } + if p.initialized { + err := p.mpv.SetProperty("volume", mpv.FORMAT_INT64, vol) + if err == nil { + p.vol = vol + } + return err + } + p.vol = vol + return nil +} + +// Sets the ReplayGain options of the player. +// Unlike most Player functions, SetReplayGainOptions can be called +// before Init, to set the initial replaygain options of the player on startup. +func (p *Player) SetReplayGainOptions(options player.ReplayGainOptions) error { + p.replayGainOpts = options + p.haveRGainOpts = true + mode := "no" + switch options.Mode { + case player.ReplayGainAlbum: + mode = "album" + case player.ReplayGainTrack: + mode = "track" + } + + if p.initialized { + if err := p.mpv.SetPropertyString("replaygain", mode); err != nil { + return err + } + if err := p.mpv.SetProperty("replaygain-preamp", mpv.FORMAT_DOUBLE, options.PreampGain); err != nil { + return err + } + clip := "yes" + if options.PreventClipping { + clip = "no" + } + if err := p.mpv.SetPropertyString("replaygain-clip", clip); err != nil { + return err + } + } + return nil +} + +// Sets the audio exclusive option of the player. +// Unlike most Player functions, SetAudioExclusive can be called +// before Init, to set the initial option of the player on startup. +func (p *Player) SetAudioExclusive(tf bool) { + p.audioExclusive = tf + if p.initialized { + val := "no" + if tf { + val = "yes" + } + p.mpv.SetOptionString("audio-exclusive", val) + } +} + +// Gets the current volume of the player. +func (p *Player) GetVolume() int { + return p.vol +} + +// sets paused status and ensures that audio exlusive is false while paused +// (releases audio device to other players) +func (p *Player) setPaused(paused bool) error { + if !paused && p.audioExclusive { + if err := p.mpv.SetOptionString("audio-exclusive", "yes"); err != nil { + return err + } + } + err := p.mpv.SetProperty("pause", mpv.FORMAT_FLAG, paused) + if err == nil && paused && p.audioExclusive { + err = p.mpv.SetOptionString("audio-exclusive", "no") + } + return err +} + +// Start playback from the first track in the play queue. +func (p *Player) PlayFromBeginning() error { + return p.PlayTrackAt(0) +} + +// Start playback from the specified track index in the play queue. +func (p *Player) PlayTrackAt(idx int) error { + // check if we have anything to play + if c, err := p.getInt64Property("playlist-count"); err == nil && c <= int64(idx) { + return nil + } + err := p.mpv.Command([]string{"playlist-play-index", strconv.Itoa(idx)}) + if p.GetStatus().State == player.Paused { + err = p.setPaused(false) + } + if err == nil { + p.setState(player.Playing) + } + return err +} + +// Begins playback if there is anything in the play queue and player is stopped or paused. +// If player is playing, pauses playback. +func (p *Player) PlayPause() error { + if !p.initialized { + return ErrUnitialized + } + + switch p.status.State { + case player.Stopped: + // check if we have anything to play + if c, err := p.getInt64Property("playlist-count"); err == nil && c > 0 { + err := p.mpv.Command([]string{"playlist-play-index", "0"}) + if err == nil { + p.setState(player.Playing) + } + return err + } + return nil + case player.Playing: + return p.Pause() + case player.Paused: + return p.Continue() + default: + return errors.New("Unknown player state") + } +} + +// Pause playback and update the player state +func (p *Player) Pause() error { + if p.status.State != player.Playing { + return nil + } + err := p.setPaused(true) + if err == nil { + p.prePausedState = p.status.State + p.setState(player.Paused) + } + return err +} + +// Continue playback and update the player state +func (p *Player) Continue() error { + if p.status.State == player.Paused { + err := p.setPaused(false) + if err == nil { + p.setState(p.prePausedState) + } + return err + } else if p.status.State == player.Stopped { + return p.PlayFromBeginning() + } + + return nil +} + +// Get the loop mode of the player. +func (p *Player) GetLoopMode() player.LoopMode { + return p.loopMode +} + +// Set the loop mode of the player. +func (p *Player) SetLoopMode(mode player.LoopMode) error { + if !p.initialized { + return ErrUnitialized + } + + // Return early if player is already in specified mode + if mode == p.loopMode { + return nil + } + + switch mode { + case player.LoopNone: + if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil { + return err + } + if err := p.mpv.SetOptionString("loop-file", "no"); err != nil { + return err + } + case player.LoopAll: + if err := p.mpv.SetOptionString("loop-playlist", "inf"); err != nil { + return err + } + if err := p.mpv.SetOptionString("loop-file", "no"); err != nil { + return err + } + case player.LoopOne: + if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil { + return err + } + if err := p.mpv.SetOptionString("loop-file", "inf"); err != nil { + return err + } + } + p.loopMode = mode + + return nil +} + +// Change the loop mode of the player to the next one. +// Useful for toggling UI elements, to change modes without knowing the current player mode. +func (p *Player) SetNextLoopMode() error { + switch p.loopMode { + case player.LoopNone: + return p.SetLoopMode(player.LoopAll) + case player.LoopAll: + return p.SetLoopMode(player.LoopOne) + case player.LoopOne: + return p.SetLoopMode(player.LoopNone) + default: + return nil + } +} + +// Get the current status of the player. +func (p *Player) GetStatus() player.Status { + if !p.initialized { + return p.status + } + + pos, _ := p.mpv.GetProperty("playback-time", mpv.FORMAT_DOUBLE) + dur, _ := p.mpv.GetProperty("duration", mpv.FORMAT_DOUBLE) + if pos != nil { + p.status.TimePos = pos.(float64) + } + if dur != nil { + p.status.Duration = dur.(float64) + } + if playpos, err := p.getInt64Property("playlist-pos"); err == nil { + p.status.PlaylistPos = playpos + } + return p.status +} + +// List available audio devices. +func (p *Player) ListAudioDevices() ([]AudioDevice, error) { + n, err := p.mpv.GetProperty("audio-device-list", mpv.FORMAT_NODE) + if err != nil { + return nil, err + } + nodeArr := n.(*mpv.Node).Data.([]*mpv.Node) + + devices := make([]AudioDevice, len(nodeArr)) + for i, node := range nodeArr { + dev := node.Data.(map[string]*mpv.Node) + name := dev["name"].Data.(string) + desc := dev["description"].Data.(string) + devices[i] = AudioDevice{Name: name, Description: desc} + } + return devices, nil +} + +func (p *Player) SetAudioDevice(deviceName string) error { + return p.mpv.SetPropertyString("audio-device", deviceName) +} + +func (p *Player) SetEqualizer(eq Equalizer) error { + p.equalizer = eq + if eq == nil || !eq.IsEnabled() { + return p.mpv.SetPropertyString("af", "") + } + af := "" + if math.Abs(eq.Preamp()) > 0.01 { + af = fmt.Sprintf("volume=volume=%0.1fdB", eq.Preamp()) + } + eqAF := eq.Curve().String() + if af == "" { + af = eqAF + } else if eqAF != "" { + af = fmt.Sprintf("%s,%s", af, eqAF) + } + return p.mpv.SetPropertyString("af", af) +} + +func (p *Player) Equalizer() Equalizer { + return p.equalizer +} + +func (p *Player) GetMediaInfo() (MediaInfo, error) { + var info MediaInfo + n, err := p.mpv.GetProperty("audio-params", mpv.FORMAT_NODE) + if err != nil { + return info, err + } + nodeMap := n.(*mpv.Node).Data.(map[string]*mpv.Node) + info.Format = nodeMap["format"].Data.(string) + info.Samplerate = int(nodeMap["samplerate"].Data.(int64)) + info.ChannelCount = int(nodeMap["channel-count"].Data.(int64)) + + br, err := p.mpv.GetProperty("audio-bitrate", mpv.FORMAT_INT64) + if err == nil { + info.Bitrate = int(br.(int64)) + } + codec, err := p.mpv.GetProperty("track-list/0/codec", mpv.FORMAT_STRING) + if err == nil { + info.Codec = codec.(string) + } + + return info, nil +} + +func (p *Player) getInt64Property(propName string) (int64, error) { + playpos, err := p.mpv.GetProperty(propName, mpv.FORMAT_INT64) + if err != nil { + return -1, err + } + if playpos != nil { + return playpos.(int64), nil + } + return -1, errors.New("mpv did not report playlist pos") +} + +// Returns true if a seek is currently in progress. +func (p *Player) IsSeeking() bool { + return p.seeking && p.status.State == player.Playing +} + +// Registers a callback which is invoked when the player transitions to the Paused state. +func (p *Player) OnPaused(cb func()) { + p.onPaused = append(p.onPaused, cb) +} + +// Registers a callback which is invoked when the player transitions to the Stopped state. +func (p *Player) OnStopped(cb func()) { + p.onStopped = append(p.onStopped, cb) +} + +// Registers a callback which is invoked when the player transitions to the Playing state. +func (p *Player) OnPlaying(cb func()) { + p.onPlaying = append(p.onPlaying, cb) +} + +// Registers a callback which is invoked whenever a seek event occurs. +func (p *Player) OnSeek(cb func()) { + p.onSeek = append(p.onSeek, cb) +} + +// Registers a callback which is invoked when the currently playing track changes, +// or when playback begins at any time from the Stopped state. +// Callback is invoked with the index of the currently playing track (zero-based). +func (p *Player) OnTrackChange(cb func(int64)) { + p.onTrackChange = append(p.onTrackChange, cb) +} + +// Destroy the player. +func (p *Player) Destroy() { + if p.bgCancel != nil { + p.bgCancel() + } + if p.initialized { + p.mpv.Command([]string{"stop"}) + p.mpv.TerminateDestroy() + p.initialized = false + } +} + +// sets the state and invokes callbacks, if triggered +func (p *Player) setState(s player.State) { + switch { + case s == player.Playing && p.status.State != player.Playing: + defer func() { + for _, cb := range p.onPlaying { + cb() + } + }() + case s == player.Paused && p.status.State != player.Paused: + defer func() { + for _, cb := range p.onPaused { + cb() + } + }() + case s == player.Stopped && p.status.State != player.Stopped: + defer func() { + for _, cb := range p.onStopped { + cb() + } + }() + } + p.status.State = s +} + +func (p *Player) eventHandler(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + default: + e := p.mpv.WaitEvent(1 /*timeout seconds*/) + if e.Event_Id != mpv.EVENT_NONE { + //log.Printf("mpv event: %+v\n", e) + } + switch e.Event_Id { + case mpv.EVENT_PLAYBACK_RESTART: + if p.seeking { + p.seeking = false + } + case mpv.EVENT_SEEK: + for _, cb := range p.onSeek { + cb() + } + case mpv.EVENT_FILE_LOADED: + if p.status.State == player.Paused { + // seek while paused switches to a new file + // mpv does not fire seek event in this case + for _, cb := range p.onSeek { + cb() + } + } + if pos, err := p.getInt64Property("playlist-pos"); err == nil { + p.curPlaylistPos = pos + for _, cb := range p.onTrackChange { + cb(pos) + } + } + case mpv.EVENT_IDLE: + p.status.Duration = 0 + p.status.TimePos = 0 + p.setState(player.Stopped) + } + } + } +} + +func (s SeekMode) String() string { + switch s { + case SeekAbsolute: + return "absolute" + case SeekRelative: + return "relative" + case SeekAbsolutePercent: + return "absolute-percent" + case SeekRelativePercent: + return "relative-percent" + } + return "UNKNOWN_SEEK_MODE" +} diff --git a/player/player.go b/player/player.go index b9086ed..f7594c7 100644 --- a/player/player.go +++ b/player/player.go @@ -1,19 +1,5 @@ package player -import ( - "context" - "errors" - "fmt" - "log" - "math" - "strconv" - - "github.com/dweymouth/go-mpv" -) - -// Error returned by many Player functions if called before the player has not been initialized. -var ErrUnitialized error = errors.New("mpv player uninitialized") - // The playback state (Stopped, Paused, or Playing). type State int @@ -32,23 +18,21 @@ type Status struct { PlaylistPos int64 } -// Argument to Seek function (SeekAbsolute, SeekRelative, SeekAbsolutePercent, SeekRelativePercent). -type SeekMode int +// The playback loop mode (LoopNone, LoopAll, LoopOne). +type LoopMode int const ( - SeekAbsolute SeekMode = iota - SeekRelative - SeekAbsolutePercent - SeekRelativePercent + LoopNone LoopMode = iota + LoopAll + LoopOne ) -// One of "no", "track", or "album" -type ReplayGainMode string +type ReplayGainMode int const ( - ReplayGainNone ReplayGainMode = "no" - ReplayGainTrack ReplayGainMode = "track" - ReplayGainAlbum ReplayGainMode = "album" + ReplayGainNone ReplayGainMode = iota + ReplayGainTrack + ReplayGainAlbum ) // Replay Gain options (argument to SetReplayGainOptions). @@ -59,665 +43,17 @@ type ReplayGainOptions struct { // Fallback gain intentionally omitted } -// The playback loop mode (LoopNone, LoopAll, LoopOne). -type LoopMode int - -const ( - LoopNone LoopMode = iota - LoopAll - LoopOne -) - -// Information about a specific audio device. -// Returned by ListAudioDevices. -type AudioDevice struct { - // The name of the audio device. - // This is the string to pass to SetAudioDevice. - Name string - - // The description of the audio device. - // This is the friendly string that should be used in UIs. - Description string -} - -// Media information about the currently playing media. -type MediaInfo struct { - // The sample format as string. This uses the same names as used in other places of mpv. - // NOTE: this is the format that the decoder outputs, NOT necessarily the format of the file. - Format string - - // Audio samplerate. - Samplerate int - - // The number of channels. - ChannelCount int - - // The audio codec. - Codec string - - // The average bit rate in bits per second. - Bitrate int -} - -// Player encapsulates the mpv instance and provides functions -// to control it and to check its status. -type Player struct { - mpv *mpv.Mpv - initialized bool - vol int - replayGainOpts ReplayGainOptions - haveRGainOpts bool - audioExclusive bool - status Status - loopMode LoopMode - seeking bool - curPlaylistPos int64 - prePausedState State - clientName string - equalizer Equalizer - - bgCancel context.CancelFunc - - // callbacks - onPaused []func() - onStopped []func() - onPlaying []func() - onSeek []func() - onTrackChange []func(int64) -} - -// Returns a new player. -// Must call Init on the player before it is ready for playback. -func New() *Player { - return NewWithClientName("") -} - -// Same as New, but sets the application name that mpv -// reports to the system audio API. -func NewWithClientName(c string) *Player { - return &Player{ - vol: -1, // use 100 in Init - clientName: c, - } -} - -// Initializes the Player and makes it ready for playback. -// Most Player functions will return ErrUnitialized if called before Init. -func (p *Player) Init(maxCacheMB int) error { - if !p.initialized { - m := mpv.Create() - - m.SetOptionString("idle", "yes") - m.SetOptionString("video", "no") - m.SetOptionString("audio-display", "no") - m.SetOptionString("gapless-audio", "weak") - m.SetOptionString("prefetch-playlist", "yes") - m.SetOptionString("force-seekable", "yes") - m.SetOptionString("terminal", "no") - - // limit in-memory cache size - m.SetOptionString("demuxer-max-bytes", fmt.Sprintf("%dMiB", maxCacheMB)) - - if p.vol < 0 { - p.vol = 100 - } - m.SetOption("volume", mpv.FORMAT_INT64, p.vol) - - p.SetAudioExclusive(p.audioExclusive) - if p.haveRGainOpts { - p.SetReplayGainOptions(p.replayGainOpts) - } - - if p.clientName != "" { - m.SetOptionString("audio-client-name", p.clientName) - } - - if err := m.Initialize(); err != nil { - return fmt.Errorf("error initializing mpv: %s", err.Error()) - } - p.mpv = m - } - ctx, cancel := context.WithCancel(context.Background()) - go p.eventHandler(ctx) - p.bgCancel = cancel - p.initialized = true - return nil -} - -// Appends the given file to the play queue. -// Note that the Player API does not provide methods to read -// the play queue. Clients are expected to maintain their own play queue model. -func (p *Player) AppendFile(url string) error { - log.Printf("Adding playback URL: %s", url) - if !p.initialized { - return ErrUnitialized - } - return p.mpv.Command([]string{"loadfile", url, "append"}) -} - -// Plays the specified file, clearing the previous play queue, if any. -func (p *Player) PlayFile(url string) error { - log.Printf("Adding playback URL: %s", url) - if !p.initialized { - return ErrUnitialized - } - err := p.mpv.Command([]string{"loadfile", url, "replace"}) - if err == nil { - p.setState(Playing) - } - return err -} - -// Removes the item at the given index from the internal playqueue. -func (p *Player) RemoveTrackAt(idx int) error { - if !p.initialized { - return ErrUnitialized - } - return p.mpv.Command([]string{"playlist-remove", strconv.Itoa(idx)}) -} - -// Stops playback and clears the play queue. -func (p *Player) Stop() error { - if !p.initialized { - return ErrUnitialized - } - var err error - if p.status.State == Stopped { - err = p.mpv.Command([]string{"playlist-clear"}) - } else { - if err = p.mpv.Command([]string{"stop"}); err == nil { - // if player was paused, stop command actually doesn't clear pause state - err = p.setPaused(false) - } - } - if err == nil { - p.setState(Stopped) - } - return err -} - -// Clears the play queue, except for the currently playing file. -func (p *Player) ClearPlayQueue() error { - if !p.initialized { - return ErrUnitialized - } - return p.mpv.Command([]string{"playlist-clear"}) -} - -// Seeks within the currently playing track. -// See MPV seek command documentation for more details. -func (p *Player) Seek(target string, mode SeekMode) error { - if !p.initialized { - return ErrUnitialized - } - p.seeking = true - err := p.mpv.Command([]string{"seek", target, mode.String()}) - return err -} - -// Seeks to the beginning of the current track if: -// - The current track is the first track in the play queue, or -// - The current time is more than 3 seconds past the beginning of the track. -// -// Else seeks to the beginning of the previous track. -func (p *Player) SeekBackOrPrevious() error { - if !p.initialized { - return ErrUnitialized - } - - if pos, err := p.getInt64Property("time-pos"); err == nil && pos > 3 { - return p.Seek("0", SeekAbsolutePercent) - } - if pos, err := p.getInt64Property("playlist-pos"); err == nil && pos == 0 { - return p.Seek("0", SeekAbsolutePercent) - } - return p.mpv.Command([]string{"playlist-prev"}) -} - -// Seeks to the next track in the play queue, if any. -func (p *Player) SeekNext() error { - if !p.initialized { - return ErrUnitialized - } - return p.mpv.Command([]string{"playlist-next"}) -} - -// Sets the volume of the player (0-100). -// Unlike most Player functions, SetVolume can be called before Init, -// to set the initial volume of the player on startup. -func (p *Player) SetVolume(vol int) error { - if vol > 100 { - vol = 100 - } else if vol < 0 { - vol = 0 - } - if p.initialized { - err := p.mpv.SetProperty("volume", mpv.FORMAT_INT64, vol) - if err == nil { - p.vol = vol - } - return err - } - p.vol = vol - return nil -} - -// Sets the ReplayGain options of the player. -// Unlike most Player functions, SetReplayGainOptions can be called -// before Init, to set the initial replaygain options 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", mpv.FORMAT_DOUBLE, options.PreampGain); err != nil { - return err - } - clip := "yes" - if options.PreventClipping { - clip = "no" - } - if err := p.mpv.SetPropertyString("replaygain-clip", clip); err != nil { - return err - } - } - return nil -} - -// Sets the audio exclusive option of the player. -// Unlike most Player functions, SetAudioExclusive can be called -// before Init, to set the initial option of the player on startup. -func (p *Player) SetAudioExclusive(tf bool) { - p.audioExclusive = tf - if p.initialized { - val := "no" - if tf { - val = "yes" - } - p.mpv.SetOptionString("audio-exclusive", val) - } -} - -// Gets the current volume of the player. -func (p *Player) GetVolume() int { - return p.vol -} - -// sets paused status and ensures that audio exlusive is false while paused -// (releases audio device to other players) -func (p *Player) setPaused(paused bool) error { - if !paused && p.audioExclusive { - if err := p.mpv.SetOptionString("audio-exclusive", "yes"); err != nil { - return err - } - } - err := p.mpv.SetProperty("pause", mpv.FORMAT_FLAG, paused) - if err == nil && paused && p.audioExclusive { - err = p.mpv.SetOptionString("audio-exclusive", "no") - } - return err -} - -// Start playback from the first track in the play queue. -func (p *Player) PlayFromBeginning() error { - return p.PlayTrackAt(0) -} - -// Start playback from the specified track index in the play queue. -func (p *Player) PlayTrackAt(idx int) error { - // check if we have anything to play - if c, err := p.getInt64Property("playlist-count"); err == nil && c <= int64(idx) { - return nil - } - err := p.mpv.Command([]string{"playlist-play-index", strconv.Itoa(idx)}) - if p.GetStatus().State == Paused { - err = p.setPaused(false) - } - if err == nil { - p.setState(Playing) - } - return err -} - -// Begins playback if there is anything in the play queue and player is stopped or paused. -// If player is playing, pauses playback. -func (p *Player) PlayPause() error { - if !p.initialized { - return ErrUnitialized - } - - switch p.status.State { - case Stopped: - // check if we have anything to play - if c, err := p.getInt64Property("playlist-count"); err == nil && c > 0 { - err := p.mpv.Command([]string{"playlist-play-index", "0"}) - if err == nil { - p.setState(Playing) - } - return err - } - return nil - case Playing: - return p.Pause() - case Paused: - return p.Continue() +func (r ReplayGainMode) String() string { + switch r { + case ReplayGainTrack: + return "track" + case ReplayGainAlbum: + return "album" default: - return errors.New("Unknown player state") + return "no" } } -// Pause playback and update the player state -func (p *Player) Pause() error { - if p.status.State != Playing { - return nil - } - err := p.setPaused(true) - if err == nil { - p.prePausedState = p.status.State - p.setState(Paused) - } - return err -} - -// Continue playback and update the player state -func (p *Player) Continue() error { - if p.status.State == Paused { - err := p.setPaused(false) - if err == nil { - p.setState(p.prePausedState) - } - return err - } else if p.status.State == Stopped { - return p.PlayFromBeginning() - } - - return nil -} - -// Get the loop mode of the player. -func (p *Player) GetLoopMode() LoopMode { - return p.loopMode -} - -// Set the loop mode of the player. -func (p *Player) SetLoopMode(mode LoopMode) error { - if !p.initialized { - return ErrUnitialized - } - - // Return early if player is already in specified mode - if mode == p.loopMode { - return nil - } - - switch mode { - case LoopNone: - if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil { - return err - } - if err := p.mpv.SetOptionString("loop-file", "no"); err != nil { - return err - } - case LoopAll: - if err := p.mpv.SetOptionString("loop-playlist", "inf"); err != nil { - return err - } - if err := p.mpv.SetOptionString("loop-file", "no"); err != nil { - return err - } - case LoopOne: - if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil { - return err - } - if err := p.mpv.SetOptionString("loop-file", "inf"); err != nil { - return err - } - } - p.loopMode = mode - - return nil -} - -// Change the loop mode of the player to the next one. -// Useful for toggling UI elements, to change modes without knowing the current player mode. -func (p *Player) SetNextLoopMode() error { - switch p.loopMode { - case LoopNone: - return p.SetLoopMode(LoopAll) - case LoopAll: - return p.SetLoopMode(LoopOne) - case LoopOne: - return p.SetLoopMode(LoopNone) - default: - return nil - } -} - -// Get the current status of the player. -func (p *Player) GetStatus() Status { - if !p.initialized { - return p.status - } - - pos, _ := p.mpv.GetProperty("playback-time", mpv.FORMAT_DOUBLE) - dur, _ := p.mpv.GetProperty("duration", mpv.FORMAT_DOUBLE) - if pos != nil { - p.status.TimePos = pos.(float64) - } - if dur != nil { - p.status.Duration = dur.(float64) - } - if playpos, err := p.getInt64Property("playlist-pos"); err == nil { - p.status.PlaylistPos = playpos - } - return p.status -} - -// List available audio devices. -func (p *Player) ListAudioDevices() ([]AudioDevice, error) { - n, err := p.mpv.GetProperty("audio-device-list", mpv.FORMAT_NODE) - if err != nil { - return nil, err - } - nodeArr := n.(*mpv.Node).Data.([]*mpv.Node) - - devices := make([]AudioDevice, len(nodeArr)) - for i, node := range nodeArr { - dev := node.Data.(map[string]*mpv.Node) - name := dev["name"].Data.(string) - desc := dev["description"].Data.(string) - devices[i] = AudioDevice{Name: name, Description: desc} - } - return devices, nil -} - -func (p *Player) SetAudioDevice(deviceName string) error { - return p.mpv.SetPropertyString("audio-device", deviceName) -} - -func (p *Player) SetEqualizer(eq Equalizer) error { - p.equalizer = eq - if eq == nil || !eq.IsEnabled() { - return p.mpv.SetPropertyString("af", "") - } - af := "" - if math.Abs(eq.Preamp()) > 0.01 { - af = fmt.Sprintf("volume=volume=%0.1fdB", eq.Preamp()) - } - eqAF := eq.Curve().String() - if af == "" { - af = eqAF - } else if eqAF != "" { - af = fmt.Sprintf("%s,%s", af, eqAF) - } - return p.mpv.SetPropertyString("af", af) -} - -func (p *Player) Equalizer() Equalizer { - return p.equalizer -} - -func (p *Player) GetMediaInfo() (MediaInfo, error) { - var info MediaInfo - n, err := p.mpv.GetProperty("audio-params", mpv.FORMAT_NODE) - if err != nil { - return info, err - } - nodeMap := n.(*mpv.Node).Data.(map[string]*mpv.Node) - info.Format = nodeMap["format"].Data.(string) - info.Samplerate = int(nodeMap["samplerate"].Data.(int64)) - info.ChannelCount = int(nodeMap["channel-count"].Data.(int64)) - - br, err := p.mpv.GetProperty("audio-bitrate", mpv.FORMAT_INT64) - if err == nil { - info.Bitrate = int(br.(int64)) - } - codec, err := p.mpv.GetProperty("track-list/0/codec", mpv.FORMAT_STRING) - if err == nil { - info.Codec = codec.(string) - } - - return info, nil -} - -func (p *Player) getInt64Property(propName string) (int64, error) { - playpos, err := p.mpv.GetProperty(propName, mpv.FORMAT_INT64) - if err != nil { - return -1, err - } - if playpos != nil { - return playpos.(int64), nil - } - return -1, errors.New("mpv did not report playlist pos") -} - -// Returns true if a seek is currently in progress. -func (p *Player) IsSeeking() bool { - return p.seeking && p.status.State == Playing -} - -// Registers a callback which is invoked when the player transitions to the Paused state. -func (p *Player) OnPaused(cb func()) { - p.onPaused = append(p.onPaused, cb) -} - -// Registers a callback which is invoked when the player transitions to the Stopped state. -func (p *Player) OnStopped(cb func()) { - p.onStopped = append(p.onStopped, cb) -} - -// Registers a callback which is invoked when the player transitions to the Playing state. -func (p *Player) OnPlaying(cb func()) { - p.onPlaying = append(p.onPlaying, cb) -} - -// Registers a callback which is invoked whenever a seek event occurs. -func (p *Player) OnSeek(cb func()) { - p.onSeek = append(p.onSeek, cb) -} - -// Registers a callback which is invoked when the currently playing track changes, -// or when playback begins at any time from the Stopped state. -// Callback is invoked with the index of the currently playing track (zero-based). -func (p *Player) OnTrackChange(cb func(int64)) { - p.onTrackChange = append(p.onTrackChange, cb) -} - -// Destroy the player. -func (p *Player) Destroy() { - if p.bgCancel != nil { - p.bgCancel() - } - if p.initialized { - p.mpv.Command([]string{"stop"}) - p.mpv.TerminateDestroy() - p.initialized = false - } -} - -// sets the state and invokes callbacks, if triggered -func (p *Player) setState(s State) { - switch { - case s == Playing && p.status.State != Playing: - defer func() { - for _, cb := range p.onPlaying { - cb() - } - }() - case s == Paused && p.status.State != Paused: - defer func() { - for _, cb := range p.onPaused { - cb() - } - }() - case s == Stopped && p.status.State != Stopped: - defer func() { - for _, cb := range p.onStopped { - cb() - } - }() - } - p.status.State = s -} - -func (p *Player) eventHandler(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - default: - e := p.mpv.WaitEvent(1 /*timeout seconds*/) - if e.Event_Id != mpv.EVENT_NONE { - //log.Printf("mpv event: %+v\n", e) - } - switch e.Event_Id { - case mpv.EVENT_PLAYBACK_RESTART: - if p.seeking { - p.seeking = false - } - case mpv.EVENT_SEEK: - for _, cb := range p.onSeek { - cb() - } - case mpv.EVENT_FILE_LOADED: - if p.status.State == Paused { - // seek while paused switches to a new file - // mpv does not fire seek event in this case - for _, cb := range p.onSeek { - cb() - } - } - if pos, err := p.getInt64Property("playlist-pos"); err == nil { - p.curPlaylistPos = pos - for _, cb := range p.onTrackChange { - cb(pos) - } - } - case mpv.EVENT_IDLE: - p.status.Duration = 0 - p.status.TimePos = 0 - p.setState(Stopped) - } - } - } -} - -func (s SeekMode) String() string { - switch s { - case SeekAbsolute: - return "absolute" - case SeekRelative: - return "relative" - case SeekAbsolutePercent: - return "absolute-percent" - case SeekRelativePercent: - return "relative-percent" - } - return "UNKNOWN_SEEK_MODE" -} - func (l LoopMode) String() string { switch l { case LoopNone: diff --git a/ui/bottompanel.go b/ui/bottompanel.go index 107afb0..f77c92c 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -8,7 +8,7 @@ import ( "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" - "github.com/dweymouth/supersonic/player" + "github.com/dweymouth/supersonic/player/mpv" "github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/widgets" @@ -34,7 +34,7 @@ type BottomPanel struct { var _ fyne.Widget = (*BottomPanel)(nil) -func NewBottomPanel(p *player.Player, pm *backend.PlaybackManager, contr *controller.Controller) *BottomPanel { +func NewBottomPanel(p *mpv.Player, pm *backend.PlaybackManager, contr *controller.Controller) *BottomPanel { bp := &BottomPanel{playbackManager: pm} bp.ExtendBaseWidget(bp) @@ -93,7 +93,7 @@ func NewBottomPanel(p *player.Player, pm *backend.PlaybackManager, contr *contro p.SeekBackOrPrevious() }) bp.Controls.OnSeek(func(f float64) { - p.Seek(fmt.Sprintf("%d", int(f*100)), player.SeekAbsolutePercent) + p.Seek(fmt.Sprintf("%d", int(f*100)), mpv.SeekAbsolutePercent) }) bp.AuxControls = widgets.NewAuxControls(p.GetVolume()) diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 5884c5f..c469577 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -8,6 +8,7 @@ import ( "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/player" + "github.com/dweymouth/supersonic/player/mpv" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/layouts" @@ -41,7 +42,7 @@ type nowPlayingPageState struct { pool *util.WidgetPool conf *backend.NowPlayingPageConfig pm *backend.PlaybackManager - p *player.Player + p *mpv.Player canRate bool } @@ -51,7 +52,7 @@ func NewNowPlayingPage( pool *util.WidgetPool, conf *backend.NowPlayingPageConfig, pm *backend.PlaybackManager, - p *player.Player, // TODO: once other player backends are supported (eg uPnP), refactor + p *mpv.Player, // TODO: once other player backends are supported (eg uPnP), refactor canRate bool, ) *NowPlayingPage { a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{ diff --git a/ui/controller/controller.go b/ui/controller/controller.go index c2adb4b..a7ba092 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -13,6 +13,7 @@ import ( "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/player" + "github.com/dweymouth/supersonic/player/mpv" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/dialogs" "github.com/dweymouth/supersonic/ui/util" @@ -474,7 +475,7 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map devs, err := c.App.Player.ListAudioDevices() if err != nil { log.Printf("error listing audio devices: %v", err) - devs = []player.AudioDevice{{Name: "auto", Description: "Autoselect device"}} + devs = []mpv.AudioDevice{{Name: "auto", Description: "Autoselect device"}} } bands := c.App.Player.Equalizer().BandFrequencies() @@ -491,7 +492,7 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map dlg.OnThemeSettingChanged = themeUpdateCallbk dlg.OnEqualizerSettingsChanged = func() { // currently we only have one equalizer type - eq := c.App.Player.Equalizer().(*player.ISO15BandEqualizer) + eq := c.App.Player.Equalizer().(*mpv.ISO15BandEqualizer) eq.Disabled = !c.App.Config.LocalPlayback.EqualizerEnabled eq.EQPreamp = c.App.Config.LocalPlayback.EqualizerPreamp copy(eq.BandGains[:], c.App.Config.LocalPlayback.GraphicEqualizerBands) diff --git a/ui/dialogs/settingsdialog.go b/ui/dialogs/settingsdialog.go index 91477e9..d8a9c09 100644 --- a/ui/dialogs/settingsdialog.go +++ b/ui/dialogs/settingsdialog.go @@ -10,7 +10,7 @@ import ( "unicode" "github.com/dweymouth/supersonic/backend" - "github.com/dweymouth/supersonic/player" + "github.com/dweymouth/supersonic/player/mpv" "github.com/dweymouth/supersonic/ui/layouts" myTheme "github.com/dweymouth/supersonic/ui/theme" "github.com/dweymouth/supersonic/ui/util" @@ -37,7 +37,7 @@ type SettingsDialog struct { OnEqualizerSettingsChanged func() config *backend.Config - audioDevices []player.AudioDevice + audioDevices []mpv.AudioDevice themeFiles map[string]string // filename -> displayName promptText *widget.RichText @@ -49,7 +49,7 @@ type SettingsDialog struct { // TODO: having this depend on the player package for the AudioDevice type is kinda gross. Refactor. func NewSettingsDialog( config *backend.Config, - audioDeviceList []player.AudioDevice, + audioDeviceList []mpv.AudioDevice, themeFileList map[string]string, equalizerBands []string, clientDecidesScrobble bool,