From 667dc35e92663785bfd0503f844eb7b4a127eef3 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 11 Oct 2026 11:28:15 +0200 Subject: [PATCH] Add Player.Pause, Player.Progress and wav.Decoder.Frames Pause and resume playback mid song, and report elapsed and total time for a progress bar. wav.Decoder.Frames returns the frame count of the data chunk. Link to tinypod, which uses them. --- README.md | 4 +++- player.go | 42 ++++++++++++++++++++++++++++++++++++++++++ player_test.go | 33 ++++++++++++++++++++++++++++++++- wav/wav.go | 8 ++++++++ wav/wav_test.go | 3 +++ 5 files changed, 88 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aed9ba9..c093495 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ player := tinyplayer.New(out) player.Play(strings.NewReader(sound)) ``` -`Play` takes any `io.Reader` with a WAV file in it. It sets the sample rate from the file and returns when all the audio is queued. Call `Stop` from another goroutine to end playback early. `SetVolume` takes 0 to 256. +`Play` takes any `io.Reader` with a WAV file in it. It sets the sample rate from the file and returns when all the audio is queued. Call `Stop` from another goroutine to end playback early, or `Pause` to pause and resume it. `Progress` returns the elapsed and total time. `SetVolume` takes 0 to 256. To embed a sound use a string so it stays in flash: @@ -117,6 +117,8 @@ tinygo flash -tags speaker -target=xiao-rp2350 ./examples/embed tinygo flash -tags fat_noexfat -target=pico ./examples/msc ``` +[tinypod](https://github.com/hybridgroup/tinypod) is an iPod classic style music player built on tinyplayer. It works with different displays, SD card or flash storage, and buttons. + The `fat_noexfat` tag leaves out exFAT support, which these small volumes do not need. ## Making WAV files diff --git a/player.go b/player.go index f468067..9dcf9df 100644 --- a/player.go +++ b/player.go @@ -4,6 +4,7 @@ package tinyplayer import ( "io" "sync/atomic" + "time" "github.com/hybridgroup/tinyplayer/wav" ) @@ -24,6 +25,9 @@ type Player struct { rate uint32 volume atomic.Int32 stop atomic.Bool + paused atomic.Bool + played atomic.Uint32 + total atomic.Uint32 samples [bufferFrames * 2]int16 frames [bufferFrames]uint32 } @@ -45,12 +49,45 @@ func (p *Player) Stop() { p.stop.Store(true) } +// Pause pauses or resumes the current Play call. It is safe to call from +// another goroutine. +func (p *Player) Pause(paused bool) { + p.paused.Store(paused) +} + +// Paused reports whether playback is paused. +func (p *Player) Paused() bool { + return p.paused.Load() +} + +func (p *Player) waitPaused() { + p.out.Enable(false) + for p.paused.Load() && !p.stop.Load() { + time.Sleep(10 * time.Millisecond) + } + p.out.Enable(true) +} + +// Progress returns how much of the current or last stream has been queued and +// its length. +func (p *Player) Progress() (elapsed, total time.Duration) { + rate := time.Duration(p.rate) + if rate == 0 { + return 0, 0 + } + return time.Duration(p.played.Load()) * time.Second / rate, + time.Duration(p.total.Load()) * time.Second / rate +} + // Play decodes the WAV stream in r and returns once all of it is queued. func (p *Player) Play(r io.Reader) error { p.stop.Store(false) + p.played.Store(0) + p.total.Store(0) if err := p.dec.Reset(r); err != nil { return err } + p.total.Store(uint32(p.dec.Frames())) f := p.dec.Format() if f.SampleRate != p.rate { if err := p.out.SetSampleFrequency(f.SampleRate); err != nil { @@ -60,6 +97,10 @@ func (p *Player) Play(r io.Reader) error { } ch := f.Channels for !p.stop.Load() { + if p.paused.Load() { + p.waitPaused() + continue + } n, err := p.dec.Read(p.samples[:bufferFrames*ch]) if n > 0 { vol := p.volume.Load() @@ -74,6 +115,7 @@ func (p *Player) Play(r io.Reader) error { if _, werr := p.out.WriteStereo(p.frames[:frames]); werr != nil { return werr } + p.played.Add(uint32(frames)) } if err == io.EOF { return nil diff --git a/player_test.go b/player_test.go index e9b26c6..f7df0cc 100644 --- a/player_test.go +++ b/player_test.go @@ -4,6 +4,7 @@ import ( "bytes" "os" "testing" + "time" "github.com/hybridgroup/tinyplayer/wav" ) @@ -12,10 +13,11 @@ type fakeOutput struct { rate uint32 frames []uint32 onWrite func() + enables []bool } func (f *fakeOutput) SetSampleFrequency(freq uint32) error { f.rate = freq; return nil } -func (f *fakeOutput) Enable(bool) {} +func (f *fakeOutput) Enable(e bool) { f.enables = append(f.enables, e) } func (f *fakeOutput) WriteStereo(b []uint32) (int, error) { f.frames = append(f.frames, b...) if f.onWrite != nil { @@ -36,6 +38,9 @@ func TestPlayMono(t *testing.T) { if err := p.Play(bytes.NewReader(data)); err != nil { t.Fatal(err) } + if el, tot := p.Progress(); el != tot || tot != time.Duration(n)*time.Second/16000 { + t.Fatalf("progress %v of %v", el, tot) + } if out.rate != 16000 || len(out.frames) != n { t.Fatalf("rate %d frames %d, want 16000 and %d", out.rate, len(out.frames), n) } @@ -65,3 +70,29 @@ func TestPlayStereoStop(t *testing.T) { t.Fatalf("frame 0 = %08x, want L %d R %d", out.frames[0], s[0], s[1]) } } + +func TestPause(t *testing.T) { + data, _ := os.ReadFile("wav/testdata/mono_s16_list.wav") + ref := &fakeOutput{} + New(ref).Play(bytes.NewReader(data)) + + out := &fakeOutput{} + p := New(out) + out.onWrite = func() { + out.onWrite = nil + p.Pause(true) + go func() { + time.Sleep(30 * time.Millisecond) + p.Pause(false) + }() + } + if err := p.Play(bytes.NewReader(data)); err != nil { + t.Fatal(err) + } + if len(out.frames) != len(ref.frames) { + t.Fatalf("got %d frames, want %d", len(out.frames), len(ref.frames)) + } + if len(out.enables) != 2 || out.enables[0] || !out.enables[1] { + t.Fatalf("enables %v", out.enables) + } +} diff --git a/wav/wav.go b/wav/wav.go index b81563e..7a180c7 100644 --- a/wav/wav.go +++ b/wav/wav.go @@ -42,6 +42,7 @@ type Decoder struct { r io.Reader format Format remaining uint32 + frames int buf [bufSize]byte block []byte @@ -62,11 +63,17 @@ func (d *Decoder) Format() Format { return d.format } +// Frames returns the number of sample frames in the data chunk. +func (d *Decoder) Frames() int { + return d.frames +} + // Reset reads a new WAV header from r so the decoder can be reused. func (d *Decoder) Reset(r io.Reader) error { d.r = r d.format = Format{} d.remaining = 0 + d.frames = 0 d.pos, d.end = 0, 0 hdr := d.buf[:12] @@ -170,6 +177,7 @@ func (d *Decoder) setup() error { default: return ErrUnsupported } + d.frames = int(d.remaining) / f.BlockAlign * f.SamplesPerBlock return nil } diff --git a/wav/wav_test.go b/wav/wav_test.go index 0d269ea..35eb9f1 100644 --- a/wav/wav_test.go +++ b/wav/wav_test.go @@ -105,6 +105,9 @@ func TestReuse(t *testing.T) { if err := d.Reset(bytes.NewReader(in)); err != nil { t.Fatal(err) } + if d.Frames() != 24408/4 { + t.Fatalf("pass %d: Frames %d", i, d.Frames()) + } if n := len(readAll(t, &d, 256)); n != 24408/2 { t.Fatalf("pass %d: %d samples", i, n) }