Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions player.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package tinyplayer
import (
"io"
"sync/atomic"
"time"

"github.com/hybridgroup/tinyplayer/wav"
)
Expand All @@ -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
}
Expand All @@ -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 {
Expand All @@ -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()
Expand All @@ -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
Expand Down
33 changes: 32 additions & 1 deletion player_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"os"
"testing"
"time"

"github.com/hybridgroup/tinyplayer/wav"
)
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
}
8 changes: 8 additions & 0 deletions wav/wav.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Decoder struct {
r io.Reader
format Format
remaining uint32
frames int
buf [bufSize]byte

block []byte
Expand All @@ -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]
Expand Down Expand Up @@ -170,6 +177,7 @@ func (d *Decoder) setup() error {
default:
return ErrUnsupported
}
d.frames = int(d.remaining) / f.BlockAlign * f.SamplesPerBlock
return nil
}

Expand Down
3 changes: 3 additions & 0 deletions wav/wav_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading