Skip to content

Commit

Permalink
[feature]<process>: support fast forward/backward (#14)
Browse files Browse the repository at this point in the history
Signed-off-by: o98k-ok <hggend@gmail.com>
  • Loading branch information
o98k-ok authored Mar 13, 2024
1 parent 940313e commit a2d1b96
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 4 deletions.
211 changes: 211 additions & 0 deletions cmd/samples/custom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package main

import (
"fmt"
"os"
"time"
"unicode"

"github.com/faiface/beep"
"github.com/faiface/beep/effects"
"github.com/faiface/beep/speaker"
"github.com/faiface/beep/wav"
"github.com/gdamore/tcell"
)

func drawTextLine(screen tcell.Screen, x, y int, s string, style tcell.Style) {
for _, r := range s {
screen.SetContent(x, y, r, nil, style)
x++
}
}

type audioPanel struct {
sampleRate beep.SampleRate
streamer beep.StreamSeeker
ctrl *beep.Ctrl
resampler *beep.Resampler
volume *effects.Volume
}

func newAudioPanel(sampleRate beep.SampleRate, streamer beep.StreamSeeker) *audioPanel {
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}
}

func (ap *audioPanel) play() {
speaker.Play(ap.volume)
}

func (ap *audioPanel) draw(screen tcell.Screen) {
mainStyle := tcell.StyleDefault.
Background(tcell.NewHexColor(0x473437)).
Foreground(tcell.NewHexColor(0xD7D8A2))
statusStyle := mainStyle.
Foreground(tcell.NewHexColor(0xDDC074)).
Bold(true)

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)

speaker.Lock()
position := ap.sampleRate.D(ap.streamer.Position())
length := ap.sampleRate.D(ap.streamer.Len())
volume := ap.volume.Volume
speed := ap.resampler.Ratio()
speaker.Unlock()

positionStatus := fmt.Sprintf("%v / %v", position.Round(time.Second), length.Round(time.Second))
volumeStatus := fmt.Sprintf("%.1f", volume)
speedStatus := fmt.Sprintf("%.3fx", speed)

drawTextLine(screen, 0, 5, "Position (Q/W):", mainStyle)
drawTextLine(screen, 16, 5, positionStatus, statusStyle)

drawTextLine(screen, 0, 6, "Volume (A/S):", mainStyle)
drawTextLine(screen, 16, 6, volumeStatus, statusStyle)

drawTextLine(screen, 0, 7, "Speed (Z/X):", mainStyle)
drawTextLine(screen, 16, 7, speedStatus, statusStyle)
}

func (ap *audioPanel) handle(event tcell.Event) (changed, quit bool) {
switch event := event.(type) {
case *tcell.EventKey:
if event.Key() == tcell.KeyESC {
return false, true
}

if event.Key() != tcell.KeyRune {
return false, false
}

switch unicode.ToLower(event.Rune()) {
case ' ':
speaker.Lock()
ap.ctrl.Paused = !ap.ctrl.Paused
speaker.Unlock()
return false, false

case 'q', 'w':
speaker.Lock()
newPos := ap.streamer.Position()
if event.Rune() == 'q' {
newPos -= ap.sampleRate.N(time.Second)
}
if event.Rune() == 'w' {
newPos += ap.sampleRate.N(time.Second)
}
if newPos < 0 {
newPos = 0
}
if newPos >= ap.streamer.Len() {
newPos = ap.streamer.Len() - 1
}
if err := ap.streamer.Seek(newPos); err != nil {
report(err)
}
speaker.Unlock()
return true, false

case 'a':
speaker.Lock()
ap.volume.Volume -= 0.1
speaker.Unlock()
return true, false

case 's':
speaker.Lock()
ap.volume.Volume += 0.1
speaker.Unlock()
return true, false

case 'z':
speaker.Lock()
ap.resampler.SetRatio(ap.resampler.Ratio() * 15 / 16)
speaker.Unlock()
return true, false

case 'x':
speaker.Lock()
ap.resampler.SetRatio(ap.resampler.Ratio() * 16 / 15)
speaker.Unlock()
return true, false
}
}
return false, false
}

func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s song.mp3\n", os.Args[0])
os.Exit(1)
}
f, err := os.Open(os.Args[1])
if err != nil {
report(err)
}
streamer, format, err := wav.Decode(f)
if err != nil {
report(err)
}
defer streamer.Close()

speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/30))
screen, err := tcell.NewScreen()
if err != nil {
report(err)
}
err = screen.Init()
if err != nil {
report(err)
}
defer screen.Fini()

ap := newAudioPanel(format.SampleRate, streamer)

screen.Clear()
ap.draw(screen)
screen.Show()

ap.play()

seconds := time.Tick(time.Second)
events := make(chan tcell.Event)
go func() {
for {
events <- screen.PollEvent()
}
}()

loop:
for {
select {
case event := <-events:
changed, quit := ap.handle(event)
if quit {
break loop
}
if changed {
screen.Clear()
ap.draw(screen)
screen.Show()
}
case <-seconds:
screen.Clear()
ap.draw(screen)
screen.Show()
}
}
}

func report(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/charmbracelet/lipgloss v0.9.1
github.com/duke-git/lancet/v2 v2.2.9
github.com/faiface/beep v1.1.0
github.com/gdamore/tcell v1.3.0
github.com/olekukonko/tablewriter v0.0.5
)

Expand All @@ -18,6 +19,7 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/hajimehoshi/oto v0.7.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ github.com/duke-git/lancet/v2 v2.2.9 h1:ik02ZrFg/OU0lduLfmNqo73mAhpY2a3Fm1RUFcoE
github.com/duke-git/lancet/v2 v2.2.9/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
Expand Down
3 changes: 2 additions & 1 deletion internal/convertor/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ func (ac *AfconvertConvertor) ConvertM4AToWav(reader io.Reader, writer io.Writer
}()
defer os.Remove(dstname)

c := fmt.Sprintf("afconvert %s -f WAVE -d LEI24 %s", src.Name(), dstname)
// UI8 目前用的数据格式,有些数据格式没办法快进
c := fmt.Sprintf("afconvert %s -f WAVE -d UI8 %s", src.Name(), dstname)
if _, _, err := system.ExecCommand(c); err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions internal/music/music.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Music struct {

NextTrigger func()
PauseTrigger func()
SeekTrigger func(int) error
DurationCallback func() int
PositionCallback func() int
}
Expand Down
29 changes: 29 additions & 0 deletions internal/player/play.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,35 @@ func (vp *VoicePlayer) Pause() error {
return nil
}

func (vp *VoicePlayer) FastForward() error {
ctrl := vp.Current()
if ctrl != nil && ctrl.SeekTrigger != nil && ctrl.PositionCallback != nil && ctrl.DurationCallback != nil {
p := ctrl.PositionCallback() + ctrl.SampleRate.N(time.Second*5)
if p > ctrl.DurationCallback() {
p = ctrl.DurationCallback()
}

speaker.Lock()
ctrl.SeekTrigger(p)
speaker.Unlock()

return nil
}
return nil
}

func (vp *VoicePlayer) FastBackward() error {
ctrl := vp.Current()
if ctrl != nil && ctrl.SeekTrigger != nil && ctrl.PositionCallback != nil {
p := ctrl.PositionCallback() - ctrl.SampleRate.N(5*time.Second)
if p < 0 {
p = 0
}
return ctrl.SeekTrigger(p)
}
return nil
}

func (vp *VoicePlayer) Next() (*music.Music, error) {
ctrl := vp.Current()
if ctrl != nil && ctrl.NextTrigger != nil {
Expand Down
1 change: 1 addition & 0 deletions internal/player/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func (q *StreamerQueue) Add(elem *list.Element) {
// setting music trigger
data.NextTrigger = c2.Send
data.PauseTrigger = c1.Send
data.SeekTrigger = streamer.Seek
data.DurationCallback = streamer.Len
data.PositionCallback = streamer.Position
data.SampleRate = format.SampleRate
Expand Down
2 changes: 1 addition & 1 deletion internal/ui/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (hl *HistoryList) Init() tea.Cmd {
}

func (hl *HistoryList) View() string {
help := "enter play • ↓/j move down • ↑/k move up\n backspace del music • ← page left • → page right"
help := "enter play • ↓/j move down • ↑/k move up\nbackspace del music • ← page left • → page right"

size := math.Ceil(float64(hl.player.PlayList.Len()) / float64(hl.limit))
pageInfo := fmt.Sprintf("%d • %d/%d页", hl.list.table.Cursor()+1, hl.page, int(size))
Expand Down
2 changes: 1 addition & 1 deletion internal/ui/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (ie *InputElem) View() string {

left1 := "\n" + lipgloss.JoinHorizontal(lipgloss.Center, "🥳 ", box)
left2 := ie.result.View()
help := "> switch mode • ↓ move down • ↑ move up\n ← page left • → page right • tab next menu"
help := "> switch mode • ↓ move down • ↑ move up\n← page left • → page right • tab next menu"
left3 := lipgloss.NewStyle().Bold(true).Render(help)
left := lipgloss.JoinVertical(lipgloss.Right, left1, " ", left2, " ", left3)

Expand Down
18 changes: 17 additions & 1 deletion internal/ui/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (pe *ProceeLineElem) View() string {
pkg.RenderWithWidth(music.Name, MaxWindowSize*0.6), "\n",
pkg.RenderWithWidth(music.Desc, MaxWindowSize*0.6), "\n",
pe.progress.ViewAs(pe.progress.Percent())+" "+music.DurationRate(), "\n",
lipgloss.NewStyle().Bold(true).Render("tab next menu • p prev • space pause/play • n next"))
lipgloss.NewStyle().Bold(true).Render("p prev • space pause/play • n next\ntab next menu • left -5s • right +5s"))
}

func (pe *ProceeLineElem) MsgKeyBindings() map[string]map[string]func(interface{}) tea.Cmd {
Expand Down Expand Up @@ -82,6 +82,22 @@ func (pe *ProceeLineElem) MsgKeyBindings() map[string]map[string]func(interface{
}
return nil
},
"left": func(i interface{}) tea.Cmd {
if !pe.active {
return nil
}

pe.player.FastBackward()
return nil
},
"right": func(i interface{}) tea.Cmd {
if !pe.active {
return nil
}

pe.player.FastForward()
return nil
},
},
}
}
Expand Down

0 comments on commit a2d1b96

Please sign in to comment.