diff --git a/backend/player/player.go b/backend/player/player.go index 5f97c16..5e5f753 100644 --- a/backend/player/player.go +++ b/backend/player/player.go @@ -4,19 +4,26 @@ import ( "context" "fmt" "os" + "reflect" "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 - streamer beep.Streamer - format beep.Format - sampleRate beep.SampleRate + ctx context.Context + state PlayerState + currentFile os.File + format beep.Format + baseStreamer beep.Streamer + resampled beep.Streamer + control beep.Ctrl + volume effects.Volume + queue []string } type PlayerState int @@ -27,48 +34,134 @@ const ( Stopped ) +const testQueue = []string{ + "test_data/music_library_test/gnomed.mp3", + "test_data/music_library_test/01 Some Chords.mp3", + "test_data/music_library_test/03 anything.mp3", +} + func NewPlayer() (*Player, error) { return &Player{ - // TODO: variable speaker sample rates - sampleRate: beep.SampleRate(44100), + state: Stopped, + baseStreamer: generatorsilence(-1), + // Default sample rate + // TODO: read this from config + fileFormat: beep.Format{}, + queue: testQueue, }, nil } -func (p *Player) Init(ctx context.Context) { +func (p *Player) Init(ctx context.Context) error { p.ctx = ctx p.state = Stopped + p.streamer = generators.Silence(-1) - speaker.Init(p.sampleRate, p.sampleRate.N(time.Second/10)) -} - -func (p *Player) ChangeState(desiredState PlayerState) error { - return nil -} - -func (p *Player) Play(filePath string) error { - f, err := os.Open(filePath) - if err != nil { - return fmt.Errorf("failed to open file %w", err) - } - - streamer, format, err := mp3.Decode(f) - if err != nil { - return fmt.Errorf("failed to decode mp3 %w", err) - } - defer streamer.Close() - - // initialize the speaker with the sample rate from the file - err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) + // Initialize speaker + // TODO: allow user to change buffer size and speaker sample rate + err := speaker.Init(p.speakerSampleRate, p.speakerSampleRate.N(time.Second/10)) if err != nil { return fmt.Errorf("failed to initialize speaker %w", err) } + 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 resampled to match the speaker +func (p *Player) CreateStreamerFromFile(filePath string) (beep.Streamer, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("failed to open file %w", err) + } + + streamer, format, err := mp3.Decode(f) + p.fileFormat = format + if err != nil { + return nil, fmt.Errorf("failed to decode mp3 %w", err) + } + + // resample file stream to match speaker + // TODO: variable resample quality + resampled := beep.Resample(4, p.fileFormat.SampleRate, p.speakerSampleRate, streamer) + // wrap in ctrl streamer to allow play/pause + ctrl := &beep.Ctrl{Streamer: resampled} + // wrap in volume streamer + volume := &effects.Volume{ + Streamer: ctrl, + Base: 2, + Volume: 0, + Silent: false, + } + + return volume, nil +} + +func (p *Player) Play(streamer beep.Streamer) error { + p.streamer = streamer + p.state = Playing // hangs until song finishes playing done := make(chan bool) - speaker.Play(beep.Seq(streamer, beep.Callback(func() { + speaker.Play(beep.Seq(p.streamer, beep.Callback(func() { + p.state = Paused //called when streamer finishes done <- true }))) <-done return nil } + +// TODO: reduce dupilcation in pause/resume functions +func (p *Player) Pause() error { + ctrl := &beep.Ctrl{} + // checking if current streamer is a ctrl type, if not, wrap it in one + if (reflect.TypeOf(p.streamer) != reflect.TypeOf(beep.Ctrl{})) { + ctrl = &beep.Ctrl{Streamer: p.streamer} + } + speaker.Lock() + ctrl.Paused = true + speaker.Unlock() + return nil +} + +// TODO: reduce dupilcation in pause/resume functions +func (p *Player) Resume() error { + ctrl := &beep.Ctrl{} + // checking if current streamer is a ctrl type, if not, wrap it in one + if (reflect.TypeOf(p.streamer) != reflect.TypeOf(beep.Ctrl{})) { + ctrl = &beep.Ctrl{Streamer: p.streamer} + } + speaker.Lock() + ctrl.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 +*/ +// TODO: implement max/ min volume values +func (p *Player) SetVolume() error { + volume := &effects.Volume{ + Streamer: p.streamer, + Base: 2, + Volume: 0, + Silent: false, + } + p.Play(volume) + return nil +} + +func (p *Player) MuteToggle() error { + + p.streamer.Silent = !p.Streamer. + p.Play(volume) + return nil +}