diff --git a/Makefile b/Makefile index a802458..51f4a62 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ dev: - wails dev -tags webkit2_41 + WEBKIT_DISABLE_DMABUF_RENDERER=1 wails dev -tags webkit2_41 build: wails build diff --git a/backend/player/player.go b/backend/player/player.go index a066e1b..9044e26 100644 --- a/backend/player/player.go +++ b/backend/player/player.go @@ -6,13 +6,23 @@ import ( "os" "time" + "github.com/gopxl/beep" + "github.com/gopxl/beep/effects" + "github.com/gopxl/beep/generators" "github.com/gopxl/beep/mp3" "github.com/gopxl/beep/speaker" ) type Player struct { - ctx context.Context - state PlayerState + ctx context.Context + state PlayerState + currentFile *os.File + format beep.Format + baseStreamer beep.Streamer + resampled beep.Streamer + control *beep.Ctrl + volume *effects.Volume + speakerStreamer beep.Streamer } type PlayerState int @@ -23,36 +33,136 @@ const ( Stopped ) +var speakerSampleRate = beep.SampleRate(44100) + func NewPlayer() (*Player, error) { - return &Player{}, nil + return &Player{ + state: Stopped, + baseStreamer: generators.Silence(-1), + format: beep.Format{ + SampleRate: speakerSampleRate, + }, + }, nil } -func (p *Player) Init(ctx context.Context) { +func (p *Player) Init(ctx context.Context) error { p.ctx = ctx - p.state = Stopped + + // Initialize speaker + // TODO: allow user to change buffer size and speaker sample rate + err := speaker.Init(p.format.SampleRate, p.format.SampleRate.N(time.Second/10)) + if err != nil { + return fmt.Errorf("failed to initialize speaker %w", err) + } + p.updateStreamers(p.baseStreamer, p.format.SampleRate) + + return nil } -func (p *Player) ChangeState(desiredState PlayerState) error { +func (p *Player) updateStreamers(newBaseStreamer beep.Streamer, sr beep.SampleRate) error { + // set base streamer + p.baseStreamer = newBaseStreamer + + // resample file stream to match speaker + // TODO: variable resample quality + p.resampled = beep.Resample(4, sr, speakerSampleRate, p.baseStreamer) + + // wrap in ctrl streamer to allow play/pause + p.control = &beep.Ctrl{Streamer: p.resampled} + + // wrap in volume streamer + p.volume = &effects.Volume{ + Streamer: p.control, + Base: 2, + Volume: 0, + Silent: false, + } + + // set "final" streamer + p.speakerStreamer = p.volume + + return nil +} + +// TODO: proper state management extracted to function +func (p *Player) changeState(desiredState PlayerState) error { + return nil +} + +// reads a file and creates a streamer, also wraps necessary streamers +func (p *Player) LoadFile(filePath string) error { + // opening file + f, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open file %w", err) + } + + // attempt to decode mp3 file and create streamer and format data + streamer, format, err := mp3.Decode(f) + if err != nil { + return fmt.Errorf("failed to decode mp3 %w", err) + } + + p.updateStreamers(streamer, format.SampleRate) + return nil } func (p *Player) Play() error { - f, err := os.Open("test_data/music_library_test/03 anything.mp3") - if err != nil { - return fmt.Errorf("Failed to open file %w", err) - } + p.state = Playing + // hangs until song finishes playing + done := make(chan bool) + speaker.Play(beep.Seq(p.speakerStreamer, beep.Callback(func() { + p.state = Paused //called when streamer finishes + done <- true + }))) - streamer, format, err := mp3.Decode(f) - if err != nil { - return fmt.Errorf("Failed to decode mp3 %w", err) - } - defer streamer.Close() - - err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) - if err != nil { - return fmt.Errorf("Failed to initialize speaker %w", err) - } - speaker.Play(streamer) - select {} + <-done + return nil +} + +// TODO: reduce dupilcation in pause/resume functions +func (p *Player) Pause() error { + speaker.Lock() + p.control.Paused = true + speaker.Unlock() + return nil +} + +// TODO: reduce dupilcation in pause/resume functions +func (p *Player) Resume() error { + speaker.Lock() + p.control.Paused = false + speaker.Unlock() + return nil +} + +//Paraprasing info from the beep docs here: +/* +To INCREASE volume by 1 means to multiply the signal by Base. +Volume = 0 means unchanged volume. +Positive Volume value means increasing volume +Negative Volume value means decreasing volume +*/ +func (p *Player) SetVolume(desiredVolume UserVolume) error { + // clamp value between 1 and 100 + volume := clampVolume(desiredVolume) + + // Apply the volume settings + p.volume.Volume = float64(volume.ToPlayerVolume()) + p.volume.Silent = volume == MinUserVol + + return nil +} +func (p *Player) ChangeVolume(deltaVolume int) error { + return p.SetVolume(p.getUserVolume() + UserVolume(deltaVolume)) +} + +func (p *Player) getUserVolume() UserVolume { + return PlayerVolume(p.volume.Volume).ToUserVolume() +} + +func (p *Player) MuteToggle() error { + p.volume.Silent = !p.volume.Silent return nil } diff --git a/backend/player/player_test.go b/backend/player/player_test.go new file mode 100644 index 0000000..488929a --- /dev/null +++ b/backend/player/player_test.go @@ -0,0 +1,42 @@ +package player + +import ( + "context" + "testing" +) + +var testQueue = []string{ + "../../test_data/music_library_test/other_music/03 PONPONPON.mp3", + "../../test_data/music_library_test/01 Some Chords.mp3", + "../../test_data/music_library_test/03 anything.mp3", +} + +func TestPlayer(t *testing.T) { + t.Logf("Starting test") + p, err := NewPlayer() + if err != nil { + t.Errorf("could not create player\n%s", err.Error()) + t.Failed() + } + t.Logf("initializing player") + err = p.Init(context.Background()) + if err != nil { + t.Errorf("could not initialize player\n%s", err.Error()) + t.Failed() + } + + for _, track := range testQueue { + t.Logf("loading file: %s", track) + err = p.LoadFile(track) + if err != nil { + t.Errorf("could not load file\n%s\n%s", track, err.Error()) + t.Failed() + } + err = p.Play() + if err != nil { + t.Errorf("could not play file\n%s\n%s", track, err.Error()) + t.Failed() + } + } + +} diff --git a/backend/player/volume.go b/backend/player/volume.go new file mode 100644 index 0000000..1320e15 --- /dev/null +++ b/backend/player/volume.go @@ -0,0 +1,40 @@ +package player + +type UserVolume int +type PlayerVolume float64 + +const MinUserVol UserVolume = 0 +const MaxUserVol UserVolume = 100 + +const MinPlayerVol PlayerVolume = -10 +const MaxPlayerVol PlayerVolume = 10 + +func (oldVol UserVolume) ToPlayerVolume() PlayerVolume { + var newVol PlayerVolume + + if oldVol >= MinUserVol && oldVol <= MaxUserVol { + ratio := PlayerVolume(oldVol-MinUserVol) / PlayerVolume(MaxUserVol-MinUserVol) + newVol = ratio*(MaxPlayerVol-MinPlayerVol) + MinPlayerVol + } + return newVol +} + +func (oldVolFloat PlayerVolume) ToUserVolume() UserVolume { + var newVol UserVolume + + if oldVolFloat >= MinPlayerVol && oldVolFloat <= MaxPlayerVol { + ratio := (oldVolFloat - MinPlayerVol) / (MaxPlayerVol - MinPlayerVol) + newVol = UserVolume(ratio*PlayerVolume(MaxUserVol-MinUserVol)) + MinUserVol + } + return newVol +} + +func clampVolume(v UserVolume) UserVolume { + if v > MaxUserVol { + return MaxUserVol + } + if v < MinUserVol { + return MinUserVol + } + return v +} diff --git a/frontend/src/assets/images/logo-universal.png b/frontend/src/assets/images/logo-universal.png deleted file mode 100644 index d63303b..0000000 Binary files a/frontend/src/assets/images/logo-universal.png and /dev/null differ diff --git a/frontend/wailsjs/go/player/Player.d.ts b/frontend/wailsjs/go/player/Player.d.ts index fdbf8f6..333f8a9 100755 --- a/frontend/wailsjs/go/player/Player.d.ts +++ b/frontend/wailsjs/go/player/Player.d.ts @@ -7,4 +7,4 @@ export function ChangeState(arg1:player.PlayerState):Promise; export function Init(arg1:context.Context):Promise; -export function Play():Promise; +export function Play(arg1:string):Promise; diff --git a/frontend/wailsjs/go/player/Player.js b/frontend/wailsjs/go/player/Player.js index f2ce5c0..c974a3d 100755 --- a/frontend/wailsjs/go/player/Player.js +++ b/frontend/wailsjs/go/player/Player.js @@ -10,6 +10,6 @@ export function Init(arg1) { return window['go']['player']['Player']['Init'](arg1); } -export function Play() { - return window['go']['player']['Player']['Play'](); +export function Play(arg1) { + return window['go']['player']['Player']['Play'](arg1); }