Refactor
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
This commit is contained in:
Vendored
+3
-3
@@ -113,8 +113,8 @@ func (p *Provider) AddTrack(playlistName string, track playlist.Track) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SavePlaylist overwrites the named playlist with the given tracks.
|
||||
func (p *Provider) SavePlaylist(name string, tracks []playlist.Track) error {
|
||||
// savePlaylist overwrites the named playlist with the given tracks.
|
||||
func (p *Provider) savePlaylist(name string, tracks []playlist.Track) error {
|
||||
if err := os.MkdirAll(p.dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func (p *Provider) RemoveTrack(name string, index int) error {
|
||||
if len(tracks) == 0 {
|
||||
return p.DeletePlaylist(name)
|
||||
}
|
||||
return p.SavePlaylist(name, tracks)
|
||||
return p.savePlaylist(name, tracks)
|
||||
}
|
||||
|
||||
// writeTrack writes a single [[track]] TOML section to w.
|
||||
|
||||
Vendored
+2
-2
@@ -439,7 +439,7 @@ func (p *SpotifyProvider) NewStreamer(uri string) (beep.StreamSeekCloser, beep.F
|
||||
}
|
||||
}
|
||||
|
||||
streamer := NewSpotifyStreamer(stream)
|
||||
streamer := newSpotifyStreamer(stream)
|
||||
return streamer, streamer.Format(), streamer.Duration(), nil
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@ func (p *SpotifyProvider) NewStreamer(uri string) (beep.StreamSeekCloser, beep.F
|
||||
func (p *SpotifyProvider) webAPI(ctx context.Context, method, path string, query url.Values) (*http.Response, error) {
|
||||
const maxRetries = 8
|
||||
for attempt := range maxRetries {
|
||||
resp, err := p.session.WebApi(ctx, method, path, query)
|
||||
resp, err := p.session.webApi(ctx, method, path, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Vendored
+3
-3
@@ -333,10 +333,10 @@ func (s *Session) NewStream(ctx context.Context, spotID librespot.SpotifyId, bit
|
||||
return s.player.NewStream(ctx, http.DefaultClient, spotID, bitrate, 0)
|
||||
}
|
||||
|
||||
// WebApi calls the Spotify Web API using the OAuth2 access token.
|
||||
// webApi calls the Spotify Web API using the OAuth2 access token.
|
||||
// This is the standard Web API token (not go-librespot's internal spclient token),
|
||||
// which has proper rate limits for api.spotify.com endpoints.
|
||||
func (s *Session) WebApi(ctx context.Context, method, path string, query url.Values) (*http.Response, error) {
|
||||
func (s *Session) webApi(ctx context.Context, method, path string, query url.Values) (*http.Response, error) {
|
||||
s.mu.Lock()
|
||||
ts := s.tokenSource
|
||||
s.mu.Unlock()
|
||||
@@ -394,7 +394,7 @@ func (s *Session) Close() {
|
||||
//
|
||||
// The new session is established before tearing down the old one to avoid a
|
||||
// window where s.sess/s.player are nil (which would crash concurrent callers
|
||||
// like NewStream or WebApi).
|
||||
// like NewStream or webApi).
|
||||
func (s *Session) Reconnect(ctx context.Context) error {
|
||||
// Capture clientID without holding the lock during the (potentially long)
|
||||
// interactive OAuth2 flow.
|
||||
|
||||
Vendored
+13
-13
@@ -17,10 +17,10 @@ const (
|
||||
spotifyChannels = 2
|
||||
)
|
||||
|
||||
// SpotifyStreamer bridges a go-librespot AudioSource to beep.StreamSeekCloser.
|
||||
// spotifyStreamer bridges a go-librespot AudioSource to beep.StreamSeekCloser.
|
||||
// go-librespot outputs interleaved stereo float32 at 44100Hz; this converts
|
||||
// to Beep's [][2]float64 sample format.
|
||||
type SpotifyStreamer struct {
|
||||
type spotifyStreamer struct {
|
||||
source librespot.AudioSource
|
||||
stream *librespotPlayer.Stream
|
||||
buf []float32
|
||||
@@ -28,13 +28,13 @@ type SpotifyStreamer struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// NewSpotifyStreamer wraps a go-librespot Stream as a beep.StreamSeekCloser.
|
||||
func NewSpotifyStreamer(stream *librespotPlayer.Stream) *SpotifyStreamer {
|
||||
// newSpotifyStreamer wraps a go-librespot Stream as a beep.StreamSeekCloser.
|
||||
func newSpotifyStreamer(stream *librespotPlayer.Stream) *spotifyStreamer {
|
||||
var dur int64
|
||||
if stream.Media != nil {
|
||||
dur = int64(stream.Media.Duration())
|
||||
}
|
||||
return &SpotifyStreamer{
|
||||
return &spotifyStreamer{
|
||||
source: stream.Source,
|
||||
stream: stream,
|
||||
durationMs: dur,
|
||||
@@ -43,7 +43,7 @@ func NewSpotifyStreamer(stream *librespotPlayer.Stream) *SpotifyStreamer {
|
||||
|
||||
// Stream reads interleaved float32 from the AudioSource and converts to
|
||||
// [][2]float64 stereo pairs for Beep's audio pipeline.
|
||||
func (s *SpotifyStreamer) Stream(samples [][2]float64) (n int, ok bool) {
|
||||
func (s *spotifyStreamer) Stream(samples [][2]float64) (n int, ok bool) {
|
||||
// Each stereo sample pair needs 2 float32 values (L, R).
|
||||
needed := len(samples) * spotifyChannels
|
||||
if len(s.buf) < needed {
|
||||
@@ -72,20 +72,20 @@ func (s *SpotifyStreamer) Stream(samples [][2]float64) (n int, ok bool) {
|
||||
return pairs, true
|
||||
}
|
||||
|
||||
func (s *SpotifyStreamer) Err() error { return s.err }
|
||||
func (s *spotifyStreamer) Err() error { return s.err }
|
||||
|
||||
// Len returns the total number of sample pairs (at 44100Hz stereo).
|
||||
func (s *SpotifyStreamer) Len() int {
|
||||
func (s *spotifyStreamer) Len() int {
|
||||
return int(s.durationMs * spotifySampleRate / 1000)
|
||||
}
|
||||
|
||||
// Position returns the current playback position in sample pairs.
|
||||
func (s *SpotifyStreamer) Position() int {
|
||||
func (s *spotifyStreamer) Position() int {
|
||||
return int(s.source.PositionMs() * spotifySampleRate / 1000)
|
||||
}
|
||||
|
||||
// Seek moves to sample position p (in sample pairs at 44100Hz).
|
||||
func (s *SpotifyStreamer) Seek(p int) error {
|
||||
func (s *spotifyStreamer) Seek(p int) error {
|
||||
ms := int64(p) * 1000 / spotifySampleRate
|
||||
return s.source.SetPositionMs(ms)
|
||||
}
|
||||
@@ -96,12 +96,12 @@ func (s *SpotifyStreamer) Seek(p int) error {
|
||||
// reader and decryption pipeline will be released when the object is GC'd.
|
||||
// This is a known limitation for skipped tracks until go-librespot exposes
|
||||
// Close() on the AudioSource interface.
|
||||
func (s *SpotifyStreamer) Close() error {
|
||||
func (s *spotifyStreamer) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Format returns the Beep audio format for Spotify streams.
|
||||
func (s *SpotifyStreamer) Format() beep.Format {
|
||||
func (s *spotifyStreamer) Format() beep.Format {
|
||||
return beep.Format{
|
||||
SampleRate: beep.SampleRate(spotifySampleRate),
|
||||
NumChannels: spotifyChannels,
|
||||
@@ -110,6 +110,6 @@ func (s *SpotifyStreamer) Format() beep.Format {
|
||||
}
|
||||
|
||||
// Duration returns the track duration.
|
||||
func (s *SpotifyStreamer) Duration() time.Duration {
|
||||
func (s *spotifyStreamer) Duration() time.Duration {
|
||||
return time.Duration(s.durationMs) * time.Millisecond
|
||||
}
|
||||
|
||||
+4
-4
@@ -132,7 +132,7 @@ func fetchLRCLIB(query string) ([]Line, error) {
|
||||
// Prefer synced lyrics.
|
||||
for _, r := range results {
|
||||
if r.SyncedLyrics != "" {
|
||||
return ParseLRC(r.SyncedLyrics), nil
|
||||
return parseLRC(r.SyncedLyrics), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,11 +198,11 @@ func fetchNetEase(query string) ([]Line, error) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
return ParseLRC(lyricRes.Lrc.Lyric), nil
|
||||
return parseLRC(lyricRes.Lrc.Lyric), nil
|
||||
}
|
||||
|
||||
// ParseLRC converts standard LRC string blocks into a slice of timestamped Lines.
|
||||
func ParseLRC(data string) []Line {
|
||||
// parseLRC converts standard LRC string blocks into a slice of timestamped Lines.
|
||||
func parseLRC(data string) []Line {
|
||||
var lines []Line
|
||||
for _, raw := range strings.Split(data, "\n") {
|
||||
matches := lrcRegex.FindStringSubmatch(raw)
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ import (
|
||||
"github.com/gopxl/beep/v2"
|
||||
)
|
||||
|
||||
// EQFreqs are the center frequencies for the 10-band parametric equalizer.
|
||||
var EQFreqs = [10]float64{70, 180, 320, 600, 1000, 3000, 6000, 12000, 14000, 16000}
|
||||
// eqFreqs are the center frequencies for the 10-band parametric equalizer.
|
||||
var eqFreqs = [10]float64{70, 180, 320, 600, 1000, 3000, 6000, 12000, 14000, 16000}
|
||||
|
||||
// biquad implements a second-order IIR peaking equalizer per the Audio EQ Cookbook.
|
||||
// Each filter reads its gain from a shared pointer, so EQ changes take
|
||||
|
||||
+3
-3
@@ -40,7 +40,7 @@ type Player struct {
|
||||
ctrl *beep.Ctrl
|
||||
volume atomic.Uint64 // dB stored as Float64bits, range [-30, +6]
|
||||
eqBands [10]atomic.Uint64 // dB stored as math.Float64bits
|
||||
tap *Tap
|
||||
tap *tap
|
||||
playing atomic.Bool
|
||||
paused atomic.Bool
|
||||
mono atomic.Bool
|
||||
@@ -164,11 +164,11 @@ func (p *Player) playPipeline(tp *trackPipeline) error {
|
||||
var s beep.Streamer = p.gapless
|
||||
|
||||
for i := range 10 {
|
||||
s = newBiquad(s, EQFreqs[i], 1.4, &p.eqBands[i], float64(p.sr))
|
||||
s = newBiquad(s, eqFreqs[i], 1.4, &p.eqBands[i], float64(p.sr))
|
||||
}
|
||||
|
||||
s = &volumeStreamer{s: s, vol: &p.volume, mono: &p.mono, cachedDB: math.NaN()}
|
||||
p.tap = NewTap(s, 4096)
|
||||
p.tap = newTap(s, 4096)
|
||||
p.ctrl = &beep.Ctrl{Streamer: p.tap}
|
||||
p.started = true
|
||||
p.playing.Store(true)
|
||||
|
||||
+9
-9
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/gopxl/beep/v2"
|
||||
)
|
||||
|
||||
// Tap is a streamer wrapper that copies samples into a ring buffer
|
||||
// tap is a streamer wrapper that copies samples into a ring buffer
|
||||
// for real-time FFT visualization. It sits in the audio pipeline
|
||||
// between the volume control and the speaker controller.
|
||||
//
|
||||
@@ -16,16 +16,16 @@ import (
|
||||
// (sole writer) and the UI thread (infrequent reader at 50ms intervals)
|
||||
// to operate without mutex contention. Minor sample tearing at the
|
||||
// read boundary is invisible in FFT-based spectrum visualization.
|
||||
type Tap struct {
|
||||
type tap struct {
|
||||
s beep.Streamer
|
||||
buf []float64
|
||||
pos atomic.Int64
|
||||
size int
|
||||
}
|
||||
|
||||
// NewTap wraps a streamer with a ring buffer of the given size.
|
||||
func NewTap(s beep.Streamer, bufSize int) *Tap {
|
||||
return &Tap{
|
||||
// newTap wraps a streamer with a ring buffer of the given size.
|
||||
func newTap(s beep.Streamer, bufSize int) *tap {
|
||||
return &tap{
|
||||
s: s,
|
||||
buf: make([]float64, bufSize),
|
||||
size: bufSize,
|
||||
@@ -33,7 +33,7 @@ func NewTap(s beep.Streamer, bufSize int) *Tap {
|
||||
}
|
||||
|
||||
// Stream passes audio through while capturing a mono mix into the ring buffer.
|
||||
func (t *Tap) Stream(samples [][2]float64) (int, bool) {
|
||||
func (t *tap) Stream(samples [][2]float64) (int, bool) {
|
||||
n, ok := t.s.Stream(samples)
|
||||
p := int(t.pos.Load())
|
||||
for i := range n {
|
||||
@@ -45,12 +45,12 @@ func (t *Tap) Stream(samples [][2]float64) (int, bool) {
|
||||
}
|
||||
|
||||
// Err returns the underlying streamer's error.
|
||||
func (t *Tap) Err() error {
|
||||
func (t *tap) Err() error {
|
||||
return t.s.Err()
|
||||
}
|
||||
|
||||
// Samples returns the last n samples from the ring buffer in chronological order.
|
||||
func (t *Tap) Samples(n int) []float64 {
|
||||
func (t *tap) Samples(n int) []float64 {
|
||||
if n > t.size {
|
||||
n = t.size
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func (t *Tap) Samples(n int) []float64 {
|
||||
|
||||
// SamplesInto copies the last len(dst) samples into dst, avoiding allocation.
|
||||
// Returns the number of samples written.
|
||||
func (t *Tap) SamplesInto(dst []float64) int {
|
||||
func (t *tap) SamplesInto(dst []float64) int {
|
||||
n := len(dst)
|
||||
if n > t.size {
|
||||
n = t.size
|
||||
|
||||
@@ -205,7 +205,7 @@ func TrackFromPath(path string) Track {
|
||||
if IsURL(path) {
|
||||
return trackFromURL(path)
|
||||
}
|
||||
return ReadTags(path)
|
||||
return readTags(path)
|
||||
}
|
||||
|
||||
// trackFromURL creates a Track from an HTTP/HTTPS URL, extracting a clean
|
||||
|
||||
+2
-2
@@ -8,10 +8,10 @@ import (
|
||||
"github.com/dhowden/tag"
|
||||
)
|
||||
|
||||
// ReadTags reads embedded metadata (ID3v2, Vorbis comments, MP4 atoms) from
|
||||
// readTags reads embedded metadata (ID3v2, Vorbis comments, MP4 atoms) from
|
||||
// a local audio file and returns a Track. Falls back to filename parsing if
|
||||
// tag reading fails or the tags contain no title.
|
||||
func ReadTags(path string) Track {
|
||||
func readTags(path string) Track {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return trackFromFilename(path)
|
||||
|
||||
+2
-2
@@ -121,10 +121,10 @@ func entriesToTracks(entries []m3uEntry) []playlist.Track {
|
||||
return tracks
|
||||
}
|
||||
|
||||
// ResolveLocalM3U opens a local .m3u/.m3u8 file, parses it with EXTINF
|
||||
// resolveLocalM3U opens a local .m3u/.m3u8 file, parses it with EXTINF
|
||||
// metadata, and returns the resulting tracks. Relative paths in the M3U
|
||||
// are resolved against the directory containing the M3U file.
|
||||
func ResolveLocalM3U(path string) ([]playlist.Track, error) {
|
||||
func resolveLocalM3U(path string) ([]playlist.Track, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+2
-2
@@ -139,9 +139,9 @@ func stripMirrorSuffix(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// ResolveLocalPLS opens a local .pls file, parses it, and returns the
|
||||
// resolveLocalPLS opens a local .pls file, parses it, and returns the
|
||||
// resulting tracks.
|
||||
func ResolveLocalPLS(path string) ([]playlist.Track, error) {
|
||||
func resolveLocalPLS(path string) ([]playlist.Track, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ func Args(args []string) (Result, error) {
|
||||
}
|
||||
for _, path := range matches {
|
||||
if playlist.IsLocalM3U(path) {
|
||||
tracks, err := ResolveLocalM3U(path)
|
||||
tracks, err := resolveLocalM3U(path)
|
||||
if err != nil {
|
||||
return r, fmt.Errorf("loading m3u %s: %w", path, err)
|
||||
}
|
||||
@@ -79,7 +79,7 @@ func Args(args []string) (Result, error) {
|
||||
continue
|
||||
}
|
||||
if playlist.IsLocalPLS(path) {
|
||||
tracks, err := ResolveLocalPLS(path)
|
||||
tracks, err := resolveLocalPLS(path)
|
||||
if err != nil {
|
||||
return r, fmt.Errorf("loading pls %s: %w", path, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user