Skip to content

Commit

Permalink
Support to cache audio to local, reduce the memory consume (#3)
Browse files Browse the repository at this point in the history
* Support to cache audio to local, reduce the memory consume

* Update readme file about how to release
  • Loading branch information
LinuxSuRen authored Nov 19, 2021
1 parent ad41387 commit 57a022d
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 16 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ Listen podcast via CLI.

## Get started

Install via [goget](https://github.com/linuxsuren/goget):
Install via [hd](https://github.com/linuxsuren/http-downloader):

```shell
goget github.com/linuxsuren/goplay
hd install goplay
```

Start to listen:

```shell
goplay 开源面对面
```

## Release

This project can be released via [linuxsuren-versions](https://github.com/linuxsuren/linuxsuren-versions).
50 changes: 44 additions & 6 deletions pkg/advanced_ui/track.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import (
"github.com/linuxsuren/goplay/pkg/ui"
"github.com/spf13/viper"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"strconv"
"time"
)
Expand All @@ -26,11 +28,8 @@ type TrackAudioPanel struct {
func NewTrackAudioPanel(trackInfo *broadcast.TrackInfo) (panel *TrackAudioPanel, err error) {
_ = loadConfig()

var resp *http.Response
if resp, err = http.Get(trackInfo.PlayURL64); err == nil {
data, _ := io.ReadAll(resp.Body)
buffer := bytes.NewReader(data)

var buffer io.Reader
if buffer, err = playWithLocalCache(trackInfo.PlayURL64); err == nil {
var streamer beep.StreamSeekCloser
var format beep.Format
streamer, format, err = mp3.Decode(playio.SeekerWithoutCloser(buffer))
Expand All @@ -41,13 +40,52 @@ func NewTrackAudioPanel(trackInfo *broadcast.TrackInfo) (panel *TrackAudioPanel,
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/30))

return &TrackAudioPanel{
AudioPlayer: ui.NewAudioPanel(format.SampleRate, streamer),
AudioPlayer: ui.NewAudioPanel(format.SampleRate, streamer, trackInfo.Title),
audioUID: strconv.Itoa(trackInfo.UID),
}, nil
}
return
}

func playWithLocalCache(trackURL string) (reader io.Reader, err error) {
if reader, err = playWithRange(trackURL, -1); err != nil {
return
}

var data []byte
if data, err = ioutil.ReadAll(reader); err != nil {
return
}

cache := path.Join(os.TempDir(), "1")
if err = ioutil.WriteFile(cache, data, 0600); err != nil {
return
}

reader, err = os.Open(cache)
return
}

func playWithRange(trackURL string, from int) (reader io.Reader, err error) {
var resp *http.Response
if resp, err = http.Get(trackURL); err == nil {
ranges := resp.Header.Get("Accept-Ranges")
length := resp.Header.Get("Content-Length")

if ranges == "bytes" && from >= 0 {
lenghtNum, _ := strconv.Atoi(length)
reader = playio.NewRangeReader(0, lenghtNum, trackURL)
} else {
var resp *http.Response
if resp, err = http.Get(trackURL); err == nil {
data, _ := io.ReadAll(resp.Body)
reader = bytes.NewReader(data)
}
}
}
return
}

func loadConfig() (err error) {
viper.SetConfigName("goplay")
viper.SetConfigType("yaml")
Expand Down
55 changes: 53 additions & 2 deletions pkg/io/reader.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package io

import "io"
import (
"errors"
"fmt"
"io"
"net/http"
)

// SeekableReader represents a reader that be able to seek
type SeekableReader interface {
Expand All @@ -18,9 +23,55 @@ type nopCloser struct {
io.Reader
}

func (a nopCloser) Seek(offset int64, whence int) (int64, error) {
func (a nopCloser) Seek(offset int64, whence int) (int64, error) {
seeker := a.Reader.(io.Seeker)
return seeker.Seek(offset, whence)
}

func (nopCloser) Close() error { return nil }

type RangeReader struct {
offset int
length int
bufferSize int

resource string
}

func NewRangeReader(offset, lenght int, resource string) *RangeReader {
return &RangeReader{
offset: offset,
length: lenght,
bufferSize: 0,
resource: resource,
}
}

func (r *RangeReader) Read(p []byte) (n int, err error) {
if r.bufferSize == 0 {
r.bufferSize = len(p)
}

client := http.DefaultClient

var request *http.Request
if request, err = http.NewRequest(http.MethodGet, r.resource, nil); err != nil {
return
}

request.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", r.offset, r.offset+r.bufferSize))

var resp *http.Response
if resp, err = client.Do(request); err != nil {
return
}

if resp.StatusCode == http.StatusPartialContent {
if n, err = resp.Body.Read(p); err == nil {
r.offset += r.bufferSize
}
} else {
return 0, errors.New(fmt.Sprintf("unspport status code: %d", resp.StatusCode))
}
return
}
35 changes: 29 additions & 6 deletions pkg/ui/audio_panel.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ type audioPanel struct {
ctrl *beep.Ctrl
resampler *beep.Resampler
volume *effects.Volume
title string
}

// NewAudioPanel creates a audio panel
func NewAudioPanel(sampleRate beep.SampleRate, streamer beep.StreamSeeker) AudioPlayer {
func NewAudioPanel(sampleRate beep.SampleRate, streamer beep.StreamSeeker, title string) AudioPlayer {
ctrl := &beep.Ctrl{Streamer: beep.Loop(-1, streamer)}
resampler := beep.ResampleRatio(4, 1, ctrl)
volume := &effects.Volume{Streamer: resampler, Base: 2}
return &audioPanel{sampleRate, streamer, ctrl, resampler, volume}
return &audioPanel{sampleRate, streamer, ctrl, resampler, volume, title}
}

// Play starts to play a audio
Expand All @@ -42,6 +43,31 @@ func (ap *audioPanel) Position() int {
return ap.streamer.Position()
}

type linePrinter struct {
index int
screen tcell.Screen
style tcell.Style
}

func (p *linePrinter) print(msg string) {
drawTextLine(p.screen, 0, p.index, msg, p.style)
p.index++
}

func (ap *audioPanel) printInfoArea(screen tcell.Screen, mainStyle tcell.Style) {
printer := &linePrinter{
screen: screen,
style: mainStyle,
}
printer.print("Welcome to the GoPlay!")
if ap.title != "" {
printer.print(fmt.Sprintf("Title: %s", ap.title))
}
printer.print("Press [ESC] to quit.")
printer.print("Press [SPACE] to pause/resume.")
printer.print("Use keys in (?/?) to turn the buttons.")
}

// Draw draws the infomation to a screen
func (ap *audioPanel) Draw(screen tcell.Screen) {
mainStyle := tcell.StyleDefault.
Expand All @@ -53,10 +79,7 @@ func (ap *audioPanel) Draw(screen tcell.Screen) {

screen.Fill(' ', mainStyle)

drawTextLine(screen, 0, 0, "Welcome to the Speedy Player!", mainStyle)
drawTextLine(screen, 0, 1, "Press [ESC] to quit.", mainStyle)
drawTextLine(screen, 0, 2, "Press [SPACE] to pause/resume.", mainStyle)
drawTextLine(screen, 0, 3, "Use keys in (?/?) to turn the buttons.", mainStyle)
ap.printInfoArea(screen, mainStyle)

speaker.Lock()
position := ap.sampleRate.D(ap.streamer.Position())
Expand Down

0 comments on commit 57a022d

Please sign in to comment.