refactored many events to use wails bindings, reducing boilerplate
This commit is contained in:
+24
-143
@@ -94,16 +94,10 @@ var (
|
||||
|
||||
var speakerSampleRate = beep.SampleRate(44100)
|
||||
|
||||
// NewPlayer creates a player and initializes the audio speaker.
|
||||
func NewPlayer(
|
||||
ctx context.Context,
|
||||
logger *slog.Logger,
|
||||
db *database.DB,
|
||||
) (*Player, error) {
|
||||
defer profiling.TimeOp(logger, "player.NewPlayer")()
|
||||
|
||||
player := &Player{
|
||||
ctx: ctx,
|
||||
// NewPlayer creates a player. Call InitSpeaker separately to
|
||||
// initialize the audio output device.
|
||||
func NewPlayer(logger *slog.Logger, db *database.DB) *Player {
|
||||
return &Player{
|
||||
logger: logger,
|
||||
db: db,
|
||||
state: Stopped,
|
||||
@@ -112,19 +106,27 @@ func NewPlayer(
|
||||
SampleRate: speakerSampleRate,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: allow user to change buffer size and speaker sample rate
|
||||
// InitSpeaker initializes the audio output device. This is
|
||||
// separated from NewPlayer so the player struct can be created
|
||||
// before wails.Run (for binding registration) while deferring
|
||||
// hardware initialization to OnStartup.
|
||||
func (p *Player) InitSpeaker() error {
|
||||
defer profiling.TimeOp(p.logger, "player.InitSpeaker")()
|
||||
|
||||
// TODO: allow user to change buffer size and speaker sample rate.
|
||||
err := speaker.Init(
|
||||
player.format.SampleRate,
|
||||
player.format.SampleRate.N(time.Second/10),
|
||||
p.format.SampleRate,
|
||||
p.format.SampleRate.N(time.Second/10),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
return fmt.Errorf(
|
||||
"failed to initialize speaker: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return player, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPlaybackFinishedHandler sets a callback invoked when a track
|
||||
@@ -137,140 +139,18 @@ func (p *Player) SetPlaybackFinishedHandler(handler func()) {
|
||||
p.playbackFinishedHandler = handler
|
||||
}
|
||||
|
||||
// SetContext sets the Wails context, registers event handlers, and
|
||||
// restores persisted state.
|
||||
// SetContext sets the Wails runtime context and restores persisted
|
||||
// state.
|
||||
func (p *Player) SetContext(ctx context.Context) {
|
||||
p.mu.Lock()
|
||||
p.ctx = ctx
|
||||
p.mu.Unlock()
|
||||
|
||||
p.registerEventHandlers()
|
||||
|
||||
p.mu.Lock()
|
||||
p.restoreStateLocked()
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
func (p *Player) registerEventHandlers() {
|
||||
p.mu.Lock()
|
||||
ctx := p.ctx
|
||||
p.mu.Unlock()
|
||||
|
||||
if ctx == nil {
|
||||
p.logger.Error(
|
||||
"Context is nil, cannot register event handlers",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
runtime.EventsOn(
|
||||
ctx,
|
||||
events.RequestPause,
|
||||
func(_ ...any) {
|
||||
p.logger.Info("Received RequestPauseEvent")
|
||||
|
||||
if err := p.Pause(); err != nil {
|
||||
p.logger.Error("failed to pause", "err", err)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
runtime.EventsOn(
|
||||
ctx,
|
||||
events.RequestLoadFile,
|
||||
func(data ...any) {
|
||||
p.logger.Info("Received RequestLoadFileEvent")
|
||||
|
||||
if len(data) < 1 {
|
||||
p.logger.Warn(
|
||||
"RequestLoadFile: missing file path argument",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
filePath, ok := data[0].(string)
|
||||
if !ok {
|
||||
p.logger.Warn(
|
||||
"RequestLoadFile: invalid file path type",
|
||||
"got", fmt.Sprintf("%T", data[0]),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err := p.LoadFile(filePath)
|
||||
if err != nil {
|
||||
p.logger.Error(err.Error())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
runtime.EventsOn(ctx, events.Seek, func(data ...any) {
|
||||
p.logger.Info("Received SeekEvent")
|
||||
|
||||
if len(data) < 1 {
|
||||
p.logger.Warn("Seek: missing seek value argument")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
seekFloat, ok := data[0].(float64)
|
||||
if !ok {
|
||||
p.logger.Warn(
|
||||
"Seek: invalid seek value type",
|
||||
"got", fmt.Sprintf("%T", data[0]),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
seekValue := int(seekFloat)
|
||||
|
||||
err := p.Seek(seekValue)
|
||||
if err != nil {
|
||||
p.logger.Error("cannot seek", "error", err)
|
||||
}
|
||||
})
|
||||
|
||||
runtime.EventsOn(
|
||||
ctx,
|
||||
events.RequestSetVolume,
|
||||
func(data ...any) {
|
||||
if len(data) < 1 {
|
||||
p.logger.Warn(
|
||||
"RequestSetVolume: missing volume argument",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
volFloat, ok := data[0].(float64)
|
||||
if !ok {
|
||||
p.logger.Warn(
|
||||
"RequestSetVolume: invalid volume type",
|
||||
"got", fmt.Sprintf("%T", data[0]),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
desiredVolume := UserVolume(volFloat)
|
||||
p.logger.Info(
|
||||
"Received RequestSetVolumeEvent",
|
||||
"volume", desiredVolume,
|
||||
)
|
||||
|
||||
p.mu.Lock()
|
||||
p.setVolumeLocked(desiredVolume)
|
||||
p.emitVolumeChanged()
|
||||
p.saveState()
|
||||
p.mu.Unlock()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Emit helpers (must be called with p.mu held)
|
||||
// ---------------------------------------------------------------
|
||||
@@ -695,14 +575,15 @@ func (p *Player) UnloadTrack() {
|
||||
// Volume
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// SetVolume sets the playback volume (0-100).
|
||||
func (p *Player) SetVolume(desiredVolume UserVolume) error {
|
||||
// SetVolume sets the playback volume (0-100), emits a
|
||||
// VolumeChanged event, and persists the new level.
|
||||
func (p *Player) SetVolume(desiredVolume UserVolume) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
p.setVolumeLocked(desiredVolume)
|
||||
|
||||
return nil
|
||||
p.emitVolumeChanged()
|
||||
p.saveState()
|
||||
}
|
||||
|
||||
func (p *Player) setVolumeLocked(desiredVolume UserVolume) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"testing"
|
||||
@@ -15,8 +14,8 @@ var testQueue = []string{
|
||||
|
||||
func TestPlayer(t *testing.T) {
|
||||
// This is an integration test that requires:
|
||||
// 1. A Wails runtime context (SetContext calls runtime.EventsOn)
|
||||
// 2. An audio output device (speaker.Init)
|
||||
// 1. A Wails runtime context (SetContext restores persisted state)
|
||||
// 2. An audio output device (InitSpeaker)
|
||||
//
|
||||
// Skip unless the caller explicitly opts in via YELLOWJACKET_INTEGRATION=1.
|
||||
if os.Getenv("YELLOWJACKET_INTEGRATION") == "" {
|
||||
@@ -27,25 +26,24 @@ func TestPlayer(t *testing.T) {
|
||||
|
||||
t.Logf("Starting test")
|
||||
|
||||
p, err := NewPlayer(context.Background(), slog.Default(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create player: %s", err.Error())
|
||||
p := NewPlayer(slog.Default(), nil)
|
||||
|
||||
if err := p.InitSpeaker(); err != nil {
|
||||
t.Fatalf("could not initialize speaker: %s", err.Error())
|
||||
}
|
||||
|
||||
// SetContext registers Wails event handlers; only works with a real Wails context.
|
||||
// SetContext restores persisted state; only works with a real Wails context.
|
||||
p.SetContext(t.Context())
|
||||
t.Logf("initializing player")
|
||||
|
||||
for _, track := range testQueue {
|
||||
t.Logf("loading file: %s", track)
|
||||
|
||||
err = p.LoadFile(track)
|
||||
if err != nil {
|
||||
if err := p.LoadFile(track); err != nil {
|
||||
t.Fatalf("could not load file %s: %s", track, err.Error())
|
||||
}
|
||||
|
||||
err = p.Play()
|
||||
if err != nil {
|
||||
if err := p.Play(); err != nil {
|
||||
t.Fatalf("could not play file %s: %s", track, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user