Commit c188b018 authored by Trevor Slocum's avatar Trevor Slocum

Optimize rendering

parent 5a61dd98
This document covers the [ditty](https://git.sr.ht/~tslocum/ditty) command-line options.
This document covers the [ditty](https://git.sr.ht/~tslocum/ditty) configuration options.
# TODO
# Default keybindings
WIP
* Browse: J/K, Down/Up and PgDown/PgUp
* Previous: P
* Next: N
* Select: Enter
* Pause: Space
* Volume: -/+
# config.yaml
TODO
......@@ -23,9 +23,9 @@ Choose one of the following methods:
GO111MODULE=on go get git.sr.ht/~tslocum/ditty
```
## Configure
## Documentation
See [CONFIGURATION.md](https://man.sr.ht/~tslocum/ditty/CONFIGURATION.md)
See [CONFIGURATION.md](https://man.sr.ht/~tslocum/ditty/CONFIGURATION.md) for default keybindings.
## Support
......
......@@ -45,16 +45,18 @@ type AudioFile struct {
Metadata *Metadata
}
func openFile(filePath string) (*AudioFile, error) {
func openFile(filePath string, metadata *Metadata) (*AudioFile, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
metadata := readMetadata(f)
_, err = f.Seek(0, io.SeekStart)
if err != nil {
log.Fatal(err)
if metadata == nil {
metadata = readMetadata(f)
_, err = f.Seek(0, io.SeekStart)
if err != nil {
log.Fatal(err)
}
}
var (
......@@ -106,6 +108,7 @@ func play(audioFile *AudioFile) {
}
if audioFile.Format.SampleRate != playingSampleRate {
speaker.Clear()
err := speaker.Init(audioFile.Format.SampleRate, audioFile.Format.SampleRate.N(time.Second/2))
if err != nil {
log.Fatalf("failed to initialize audio device: %s", err)
......@@ -154,7 +157,8 @@ func nextTrack() {
if mainBufferCursor-1 < len(mainBufferFiles)-1 {
mainBufferCursor++
audioFile, err := openFile(path.Join(mainBufferDirectory, selectedEntry().File.Name()))
entry := selectedEntry()
audioFile, err := openFile(path.Join(mainBufferDirectory, entry.File.Name()), entry.Metadata)
if err != nil {
return
}
......
package main
import (
"bytes"
"fmt"
"math"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
"github.com/mattn/go-runewidth"
......@@ -37,7 +39,12 @@ var (
screenWidth, screenHeight int
mainBufHeight int
statusText string
mainBuffer bytes.Buffer
mainLock = new(sync.Mutex)
statusText string
statusBuffer bytes.Buffer
statusLock = new(sync.Mutex)
)
func initTUI() error {
......@@ -97,50 +104,54 @@ func browseFolder(browse string) {
}
func updateMain() {
var titleText string
mainLock.Lock()
defer mainLock.Unlock()
mainBuffer.Reset()
var statusMessage string
if statusText != "" {
titleText = statusText
statusMessage = statusText
} else {
titleText = mainBufferDirectory
statusMessage = mainBufferDirectory
}
truncated := false
widthRequirement := 4
for {
if runewidth.StringWidth(titleText) <= screenWidth-widthRequirement || !strings.ContainsRune(titleText, os.PathSeparator) {
break
}
truncated := false
widthRequirement := 4
for {
if runewidth.StringWidth(statusMessage) <= screenWidth-widthRequirement || !strings.ContainsRune(statusMessage, os.PathSeparator) {
break
}
titleText = titleText[strings.IndexRune(titleText, '/')+1:]
statusMessage = statusMessage[strings.IndexRune(statusMessage, '/')+1:]
truncated = true
widthRequirement = 8
}
if truncated {
titleText = ".../" + titleText
}
titleText = runewidth.Truncate(titleText, screenWidth-4, "...")
truncated = true
widthRequirement = 8
}
if truncated {
mainBuffer.WriteString(".../")
}
mainbuf.SetTitle(" " + titleText + " ")
mainBuffer.WriteString(statusMessage)
var printed int
mainbuf.SetTitle(" " + runewidth.Truncate(mainBuffer.String(), screenWidth-4, "...") + " ")
mainBuffer.Reset()
var newBufferText string
var printed int
var line string
if mainBufferOrigin == 0 {
if mainBufferCursor == 0 {
newBufferText += "[::r]"
mainBuffer.WriteString("[::r]")
}
var line string
if mainBufferDirectory == "/" {
line = "./"
} else {
line = "../"
}
newBufferText += line
mainBuffer.WriteString(line)
for i := len(line); i < screenWidth-2; i++ {
newBufferText += " "
mainBuffer.WriteRune(' ')
}
if mainBufferCursor == 0 {
newBufferText += "[-]"
mainBuffer.WriteString("[-]")
}
printed++
}
......@@ -150,24 +161,23 @@ func updateMain() {
}
if printed > 0 {
newBufferText += "\n"
mainBuffer.WriteRune('\n')
}
if i == mainBufferCursor-1 {
newBufferText += "[::r]"
mainBuffer.WriteString("[::r]")
}
var line string
if entry.File.IsDir() {
line = entry.File.Name() + "/"
} else {
line = entry.String()
}
newBufferText += line
mainBuffer.WriteString(line)
for i := runewidth.StringWidth(line); i < screenWidth-2; i++ {
newBufferText += " "
mainBuffer.WriteRune(' ')
}
if i == mainBufferCursor-1 {
newBufferText += "[-]"
mainBuffer.WriteString("[-]")
}
printed++
......@@ -176,7 +186,7 @@ func updateMain() {
}
}
mainbuf.SetText(newBufferText)
mainbuf.SetText(mainBuffer.String())
}
func updateQueue() {
......@@ -184,83 +194,95 @@ func updateQueue() {
}
func updateStatus() {
statusLock.Lock()
defer statusLock.Unlock()
var sampleRate beep.SampleRate
var d time.Duration
var p time.Duration
var l time.Duration
var v float64
var topStatusExtra string
speaker.Lock()
if playingStreamer == nil {
topstatusbuf.SetText("")
bottomstatusbuf.SetText("")
var paused bool
var silent bool
if playingStreamer != nil && volume != nil && ctrl != nil {
speaker.Lock()
silent = volume.Silent
paused = ctrl.Paused
sampleRate = playingFormat.SampleRate
p = playingFormat.SampleRate.D(playingStreamer.Position()).Truncate(time.Second)
l = playingFormat.SampleRate.D(playingStreamer.Len()).Truncate(time.Second)
v = volume.Volume
speaker.Unlock()
return
}
sampleRate = playingFormat.SampleRate
d = playingFormat.SampleRate.D(playingStreamer.Position()).Truncate(time.Second)
l = playingFormat.SampleRate.D(playingStreamer.Len()).Truncate(time.Second)
v = volume.Volume
paused := ctrl.Paused
topStatusExtra = fmt.Sprintf("%dHz %s", sampleRate.N(time.Second), fileFormat(playingFileName))
if paused {
topStatusExtra = "Paused " + topStatusExtra
}
statusBuffer.Reset()
speaker.Unlock()
if paused {
statusBuffer.WriteString("Paused ")
}
statusBuffer.WriteString(fmt.Sprintf(" %dHz %s", sampleRate.N(time.Second), fileFormat(playingFileName)))
topStatus := " "
if playingFileInfo != "" {
topStatus += playingFileInfo
} else {
topStatus += playingFileName
}
topStatusMaxFileLength := screenWidth - len(topStatusExtra) - 1
if topStatusMaxFileLength >= 7 {
if len(topStatus) > topStatusMaxFileLength {
topStatus = topStatus[:topStatusMaxFileLength]
topStatusExtra := statusBuffer.String()
statusBuffer.Reset()
topStatusMaxLength := screenWidth - 2
printExtra := topStatusMaxLength >= (len(topStatusExtra)*2)+1
if printExtra {
topStatusMaxLength -= len(topStatusExtra)
}
padding := screenWidth - runewidth.StringWidth(topStatus) - len(topStatusExtra) - 1
for i := 0; i < padding; i++ {
topStatus += " "
statusBuffer.WriteRune(' ')
var trackInfo string
if playingFileInfo != "" {
trackInfo = runewidth.Truncate(playingFileInfo, topStatusMaxLength, "...")
} else {
trackInfo = runewidth.Truncate(playingFileName, topStatusMaxLength, "...")
}
statusBuffer.WriteString(trackInfo)
if printExtra {
padding := topStatusMaxLength - runewidth.StringWidth(trackInfo)
for i := 0; i < padding; i++ {
statusBuffer.WriteRune(' ')
}
topStatus += topStatusExtra
statusBuffer.WriteString(topStatusExtra)
}
topstatusbuf.SetText(statusBuffer.String())
}
topstatusbuf.SetText(topStatus)
var vol string
if volume.Silent {
vol = "Mut "
statusBuffer.Reset()
if silent {
statusBuffer.WriteString("Mut ")
for i := -7.5; i < 0.0; i += 0.5 {
vol += string(tcell.RuneHLine)
statusBuffer.WriteRune(tcell.RuneHLine)
}
} else {
vol = "Vol "
statusBuffer.WriteString("Vol ")
for i := -7.5; i < v-0.5; i += 0.5 {
vol += string(tcell.RuneHLine)
statusBuffer.WriteRune(tcell.RuneHLine)
}
vol += string(tcell.RuneBlock)
statusBuffer.WriteRune(tcell.RuneBlock)
for i := v; i < 0; i += 0.5 {
vol += string(tcell.RuneHLine)
statusBuffer.WriteRune(tcell.RuneHLine)
}
}
bottomStatus := fmt.Sprintf("%s %s", formatDuration(l), vol)
bottomStatus := fmt.Sprintf("%s %s", formatDuration(l), statusBuffer.String())
statusBuffer.Reset()
var durationIndicator string
var progressIndicator string
if paused {
durationIndicator = "||"
progressIndicator = "||"
} else {
durationIndicator = string(tcell.RuneBlock)
progressIndicator = string(tcell.RuneBlock)
}
padding := screenWidth - runewidth.StringWidth(bottomStatus) - len(formatDuration(d)) - runewidth.StringWidth(durationIndicator) - 3
position := int(float64(padding) * (float64(d) / float64(l)))
padding := screenWidth - runewidth.StringWidth(bottomStatus) - len(formatDuration(p)) - runewidth.StringWidth(progressIndicator) - 3
position := int(float64(padding) * (float64(p) / float64(l)))
if position > padding-1 {
position = padding - 1
}
......@@ -268,22 +290,22 @@ func updateStatus() {
position--
}
var durationBar string
for i := 0; i < padding; i++ {
if i == position {
durationBar += durationIndicator
statusBuffer.WriteString(progressIndicator)
} else {
durationBar += string(tcell.RuneHLine)
statusBuffer.WriteRune(tcell.RuneHLine)
}
}
seekStart = len(formatDuration(d)) + 2
seekStart = len(formatDuration(p)) + 2
seekEnd = seekStart + padding - 1
volumeStart = seekEnd + len(formatDuration(l)) + 4
volumeEnd = screenWidth - 2
bottomstatusbuf.SetText(" " + formatDuration(d) + " " + durationBar + " " + bottomStatus)
bottomstatusbuf.SetText(" " + formatDuration(p) + " " + statusBuffer.String() + " " + bottomStatus)
statusBuffer.Reset()
}
func formatDuration(d time.Duration) string {
......@@ -315,7 +337,7 @@ func selectTrack() {
return
}
audioFile, err := openFile(path.Join(mainBufferDirectory, entry.File.Name()))
audioFile, err := openFile(path.Join(mainBufferDirectory, entry.File.Name()), entry.Metadata)
if err != nil {
statusText = err.Error()
go func() {
......
......@@ -11,6 +11,10 @@ func handleKeyPress(event *tcell.EventKey) *tcell.EventKey {
audioLock.Lock()
defer audioLock.Unlock()
if volume == nil {
return nil
}
speaker.Lock()
volume.Volume -= 0.5
if volume.Volume <= -7.5 {
......@@ -18,12 +22,17 @@ func handleKeyPress(event *tcell.EventKey) *tcell.EventKey {
volume.Silent = true
}
speaker.Unlock()
updateStatus()
return nil
case '+':
audioLock.Lock()
defer audioLock.Unlock()
if ctrl == nil {
return nil
}
speaker.Lock()
volume.Volume += 0.5
if volume.Volume > 0 {
......@@ -31,15 +40,21 @@ func handleKeyPress(event *tcell.EventKey) *tcell.EventKey {
}
volume.Silent = false
speaker.Unlock()
updateStatus()
return nil
case ' ':
audioLock.Lock()
defer audioLock.Unlock()
if ctrl == nil {
return nil
}
speaker.Lock()
ctrl.Paused = !ctrl.Paused
speaker.Unlock()
updateStatus()
return nil
case 'j':
......
......@@ -7,7 +7,7 @@ func listPrevious() {
if mainBufferCursor > 0 {
mainBufferCursor--
}
updateMain()
app.QueueUpdateDraw(updateMain)
}
func listNext() {
......@@ -17,7 +17,7 @@ func listNext() {
mainBufferOrigin++
}
}
updateMain()
app.QueueUpdateDraw(updateMain)
}
func selectedEntry() *LibraryEntry {
......
......@@ -17,6 +17,7 @@ func handleMouse(event *cview.EventMouse) *cview.EventMouse {
// TODO Delay playing while cursor is moved
if mouseY-1 < len(mainBufferFiles)+1 {
mainBufferCursor = mainBufferOrigin + (mouseY - 1)
app.QueueUpdateDraw(updateMain)
go selectTrack()
}
return nil
......@@ -38,6 +39,7 @@ func handleMouse(event *cview.EventMouse) *cview.EventMouse {
seekTo := int(float64(playingStreamer.Len()) * (float64(mouseX-seekStart) / float64(seekEnd-seekStart)))
_ = playingStreamer.Seek(seekTo) // Ignore seek errors
speaker.Unlock()
app.QueueUpdateDraw(updateStatus)
return nil
} else if mouseX >= volumeStart && mouseX <= volumeEnd+1 {
......@@ -49,6 +51,7 @@ func handleMouse(event *cview.EventMouse) *cview.EventMouse {
speaker.Lock()
volume.Silent = !volume.Silent
speaker.Unlock()
app.QueueUpdateDraw(updateStatus)
} else {
speaker.Lock()
......@@ -63,8 +66,8 @@ func handleMouse(event *cview.EventMouse) *cview.EventMouse {
}
volume.Silent = setVolume <= -7.5
speaker.Unlock()
app.QueueUpdateDraw(updateStatus)
}
return nil
......
......@@ -9,6 +9,7 @@ import (
"os"
"os/signal"
"path/filepath"
"runtime/pprof"
"strings"
"syscall"
"time"
......@@ -28,6 +29,7 @@ Copyright (c) 2020 Trevor Slocum <[email protected]>
var (
printVersionInfo bool
debugAddress string
cpuProfile string
done = make(chan bool)
)
......@@ -37,6 +39,7 @@ func main() {
flag.BoolVar(&printVersionInfo, "version", false, "print version information and exit")
flag.StringVar(&debugAddress, "debug-address", "", "address to serve debug info")
flag.StringVar(&cpuProfile, "cpu-profile", "", "path to save CPU profiling")
flag.Parse()
if printVersionInfo {
......@@ -50,6 +53,19 @@ func main() {
}()
}
if cpuProfile != "" {
f, err := os.Create(cpuProfile)
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
defer f.Close()
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("could not start CPU profile: ", err)
}
defer pprof.StopCPUProfile()
}
err := initTUI()
if err != nil {
log.Fatalf("failed to initialize terminal user interface: %s", err)
......@@ -99,7 +115,7 @@ func main() {
} else {
browseFolder(filepath.Dir(startPath))
audioFile, err := openFile(strings.Join(flag.Args(), " "))
audioFile, err := openFile(strings.Join(flag.Args(), " "), nil)
if err != nil {
statusText = err.Error()
app.QueueUpdateDraw(updateMain)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment