Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 7 additions & 19 deletions backend/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"github.com/dweymouth/supersonic/backend/ipc"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/backend/player"
"github.com/dweymouth/supersonic/backend/player/mpv"
"github.com/dweymouth/supersonic/backend/util"
"github.com/dweymouth/supersonic/backend/windows"
"github.com/dweymouth/supersonic/sharedutil"
Expand Down Expand Up @@ -54,7 +53,7 @@ type App struct {
AutoEQManager *AutoEQManager
EQPresetManager *EQPresetManager
PlaybackManager *PlaybackManager
LocalPlayer *mpv.Player
LocalPlayer player.LocalPlayer
UpdateChecker UpdateChecker
MPRISHandler *MPRISHandler
WinSMTC *windows.SMTC
Expand Down Expand Up @@ -143,10 +142,10 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas
a.UpdateChecker.Start(a.bgrndCtx, 24*time.Hour)
}

if err := a.initMPV(); err != nil {
if err := a.initLocalPlayer(); err != nil {
return nil, err
}
if err := a.setupMPV(); err != nil {
if err := a.setupLocalPlayer(); err != nil {
return nil, err
}

Expand Down Expand Up @@ -359,18 +358,7 @@ func (a *App) callOnExit() error {
return nil
}

func (a *App) initMPV() error {
p := mpv.NewWithClientName(a.appName)
c := a.Config.LocalPlayback
c.InMemoryCacheSizeMB = clamp(c.InMemoryCacheSizeMB, 10, 500)
if err := p.Init(c.InMemoryCacheSizeMB); err != nil {
return fmt.Errorf("failed to initialize mpv player: %s", err.Error())
}
a.LocalPlayer = p
return nil
}

func (a *App) setupMPV() error {
func (a *App) setupLocalPlayer() error {
a.Config.LocalPlayback.Volume = clamp(a.Config.LocalPlayback.Volume, 0, 100)
a.LocalPlayer.SetVolume(a.Config.LocalPlayback.Volume)

Expand Down Expand Up @@ -419,9 +407,9 @@ func (a *App) setupMPV() error {
a.LocalPlayer.SetPauseFade(a.Config.LocalPlayback.PauseFade)

// Initialize the appropriate equalizer type based on config
var eq mpv.Equalizer
var eq player.Equalizer
if a.Config.LocalPlayback.EqualizerType == "ISO10Band" {
eq10 := &mpv.ISO10BandEqualizer{
eq10 := &player.ISO10BandEqualizer{
EQPreamp: a.Config.LocalPlayback.EqualizerPreamp,
Disabled: !a.Config.LocalPlayback.EqualizerEnabled,
}
Expand All @@ -432,7 +420,7 @@ func (a *App) setupMPV() error {
}
eq = eq10
} else {
eq15 := &mpv.ISO15BandEqualizer{
eq15 := &player.ISO15BandEqualizer{
EQPreamp: a.Config.LocalPlayback.EqualizerPreamp,
Disabled: !a.Config.LocalPlayback.EqualizerEnabled,
}
Expand Down
25 changes: 25 additions & 0 deletions backend/app_player_localav.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//go:build localav

package backend

import (
"fmt"

"github.com/dweymouth/supersonic/backend/player"
"github.com/dweymouth/supersonic/backend/player/localav"
)

// initLocalPlayer constructs and initializes the localav-backed local player.
func (a *App) initLocalPlayer() error {
p := localav.New()
if err := p.Init(); err != nil {
return fmt.Errorf("failed to initialize localav player: %s", err.Error())
}
a.LocalPlayer = p
return nil
}

// localPlayerSetup performs player-specific post-init setup (localav variant).
func (a *App) localPlayerSetup(p player.LocalPlayer) {
// localav has no extra setup beyond what setupLocalPlayer does.
}
28 changes: 28 additions & 0 deletions backend/app_player_mpv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//go:build !localav

package backend

import (
"fmt"

"github.com/dweymouth/supersonic/backend/player"
"github.com/dweymouth/supersonic/backend/player/mpv"
)

// initLocalPlayer constructs and initializes the mpv-backed local player.
func (a *App) initLocalPlayer() error {
p := mpv.NewWithClientName(a.appName)
c := a.Config.LocalPlayback
c.InMemoryCacheSizeMB = clamp(c.InMemoryCacheSizeMB, 10, 500)
if err := p.Init(c.InMemoryCacheSizeMB); err != nil {
return fmt.Errorf("failed to initialize mpv player: %s", err.Error())
}
a.LocalPlayer = p
return nil
}

// localPlayerSetup performs player-specific post-init setup (mpv variant).
// Called by setupLocalPlayer after the common device/volume/EQ setup.
func (a *App) localPlayerSetup(p player.LocalPlayer) {

Check failure on line 26 in backend/app_player_mpv.go

View workflow job for this annotation

GitHub Actions / Lint

func (*App).localPlayerSetup is unused (unused)
// mpv has no extra setup beyond what setupLocalPlayer does.
}
10 changes: 7 additions & 3 deletions backend/playbackengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -984,8 +984,8 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error {
url = filepath
}
}
if mpvP, ok := p.player.(*mpv.Player); ok && !isTrack {
mpvP.ObserveIcyRadioTitle(func(icytitle string) {
if lpP, ok := p.player.(player.LocalPlayer); ok && !isTrack {
lpP.ObserveIcyRadioTitle(func(icytitle string) {
var title, artist string
if s := strings.Split(icytitle, " - "); len(s) == 2 {
title, artist = s[1], s[0]
Expand All @@ -997,7 +997,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error {
}
})
} else if ok {
mpvP.UnobserveIcyRadioTitle()
lpP.UnobserveIcyRadioTitle()
}
if url == "" {
return errors.New("no stream URL")
Expand Down Expand Up @@ -1123,6 +1123,10 @@ func (pm *playbackEngine) invokeNoArgCallbacks(cbs []func()) {
}

func (p *playbackEngine) startPollTimePos() {
if p.cancelPollPos != nil {
return // already polling
}

ctx, cancel := context.WithCancel(p.ctx)
p.cancelPollPos = cancel
pollFrequency := 250 * time.Millisecond
Expand Down
174 changes: 174 additions & 0 deletions backend/player/equalizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package player

// Equalizer types based on the FFmpeg 'equalizer' filter.
// Defined here (rather than in the mpv sub-package) so that both the mpv
// and localav player implementations can share the same types without
// creating an import cycle.

import (
"fmt"
"math"
"strings"
)

type Equalizer interface {
IsEnabled() bool
Preamp() float64
Curve() EqualizerCurve
Type() string
// Returns the band frequencies as strings friendly for display.
BandFrequencies() []string
}

type ISO15BandEqualizer struct {
Disabled bool
EQPreamp float64
BandGains [15]float64
}

var (
iso15Bands = []string{"25", "40", "63", "100", "160", "250", "400", "630", "1k", "1.6k", "2.5k", "4k", "6.3k", "10k", "16k"}
iso15FMult = math.Pow(2, 2./3)
)

var _ Equalizer = (*ISO15BandEqualizer)(nil)

func (i *ISO15BandEqualizer) IsEnabled() bool { return !i.Disabled }
func (i *ISO15BandEqualizer) Preamp() float64 { return i.EQPreamp }

func (i *ISO15BandEqualizer) Curve() EqualizerCurve {
fC := float64(25)
curve := make([]EqualizerBand, 0, len(i.BandGains))
for _, bandGain := range i.BandGains {
curve = append(curve, EqualizerBand{
Frequency: int(math.Round(fC)),
Width: 2. / 3,
WidthType: WidthTypeOctave,
Gain: bandGain,
})
fC *= iso15FMult
}
return curve
}

func (*ISO15BandEqualizer) BandFrequencies() []string {
ret := make([]string, len(iso15Bands))
copy(ret, iso15Bands)
return ret
}

func (*ISO15BandEqualizer) Type() string { return "ISO15Band" }

type ISO10BandEqualizer struct {
Disabled bool
EQPreamp float64
BandGains [10]float64
}

var (
iso10Bands = []string{"31", "62", "125", "250", "500", "1k", "2k", "4k", "8k", "16k"}
iso10FMult = 2.0
)

var _ Equalizer = (*ISO10BandEqualizer)(nil)

func (i *ISO10BandEqualizer) IsEnabled() bool { return !i.Disabled }
func (i *ISO10BandEqualizer) Preamp() float64 { return i.EQPreamp }

func (i *ISO10BandEqualizer) Curve() EqualizerCurve {
fC := float64(31.25)
curve := make([]EqualizerBand, 0, len(i.BandGains))
for _, bandGain := range i.BandGains {
curve = append(curve, EqualizerBand{
Frequency: int(math.Round(fC)),
Width: 1.0,
WidthType: WidthTypeOctave,
Gain: bandGain,
})
fC *= iso10FMult
}
return curve
}

func (*ISO10BandEqualizer) BandFrequencies() []string {
ret := make([]string, len(iso10Bands))
copy(ret, iso10Bands)
return ret
}

func (*ISO10BandEqualizer) Type() string { return "ISO10Band" }

type WidthType int

const (
WidthTypeHz WidthType = iota
WidthTypeKhz
WidthTypeQ
WidthTypeOctave
WidthTypeSlope
)

func (w WidthType) String() string {
switch w {
case WidthTypeHz:
return "h"
case WidthTypeKhz:
return "k"
case WidthTypeQ:
return "q"
case WidthTypeOctave:
return "o"
case WidthTypeSlope:
return "s"
}
return "x"
}

type EqualizerBand struct {
Frequency int
Gain float64
Width float64
WidthType WidthType
}

func (e EqualizerBand) String() string {
if math.Abs(e.Gain) < 0.02 {
return ""
}
return fmt.Sprintf("equalizer=f=%d:g=%0.2f:t=%s:w=%0.2f",
e.Frequency, e.Gain, e.WidthType.String(), e.Width)
}

// QFactor returns the Q factor for this band, converting from whatever WidthType is used.
func (e EqualizerBand) QFactor() float64 {
switch e.WidthType {
case WidthTypeQ:
return e.Width
case WidthTypeOctave:
return 1.0 / (2.0 * math.Sinh(math.Log(2)/2.0*e.Width))
case WidthTypeHz:
if e.Width > 0 {
return float64(e.Frequency) / e.Width
}
return 1.0
default:
return 1.0
}
}

type EqualizerCurve []EqualizerBand

func (e EqualizerCurve) String() string {
var sb strings.Builder
first := true
for _, band := range e {
if s := band.String(); s != "" {
if !first {
sb.WriteString(",")
}
sb.WriteString(s)
first = false
}
}
return sb.String()
}
Loading
Loading