player

package
v1.60.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package player provides the audio engine for MP3 playback with a 10-band parametric EQ, volume control, and sample capture for visualization.

Index

Constants

This section is empty.

Variables

View Source
var SupportedExts = map[string]bool{
	".mp3":  true,
	".wav":  true,
	".flac": true,
	".ogg":  true,
	".m4a":  true,
	".aac":  true,
	".aacp": true,
	".m4b":  true,
	".alac": true,
	".wma":  true,
	".opus": true,
	".webm": true,
}

SupportedExts is the set of file extensions the player can decode.

Functions

func DeviceSampleRate

func DeviceSampleRate() int

DeviceSampleRate returns 0 on platforms where device detection is not implemented. Callers should fall back to a sensible default (e.g. 44100).

func InstallYTDLP

func InstallYTDLP() error

InstallYTDLP attempts to install yt-dlp using the system package manager. Returns nil on success. The caller should re-check YTDLPAvailable() after.

func PrepareAudioDevice

func PrepareAudioDevice(device string) func()

PrepareAudioDevice sets PIPEWIRE_NODE so the PipeWire ALSA plugin routes this process's audio to the named device. Must be called before player.New(). Returns a cleanup that restores the env.

func SetYTDLCookiesFrom

func SetYTDLCookiesFrom(browser string)

SetYTDLCookiesFrom configures yt-dlp to use cookies from the given browser for YouTube Music playback (e.g. "chrome", "firefox", "brave").

func SwitchAudioDevice

func SwitchAudioDevice(deviceName string) error

SwitchAudioDevice moves cliamp's audio stream to a different sink. Falls back to changing the system default if the stream can't be found.

func YTDLPAvailable

func YTDLPAvailable() bool

YTDLPAvailable reports whether yt-dlp is installed and on PATH.

func YtdlpInstallHint

func YtdlpInstallHint() string

YtdlpInstallHint returns a platform-specific install command suggestion.

Types

type AudioDevice

type AudioDevice struct {
	Index       int
	Name        string // internal identifier (sink name, UID, or device ID)
	Description string // human-readable label
	Active      bool   // true when this is the current default
}

AudioDevice represents an available audio output device (sink/endpoint).

func ListAudioDevices

func ListAudioDevices() ([]AudioDevice, error)

ListAudioDevices returns available output sinks via pactl. Works on PulseAudio and PipeWire (via pipewire-pulse).

type Engine

type Engine interface {
	// Playback control
	Play(path string, knownDuration time.Duration) error
	PlayYTDL(pageURL string, knownDuration time.Duration) error
	Preload(path string, knownDuration time.Duration) error
	PreloadYTDL(pageURL string, knownDuration time.Duration) error
	ClearPreload()
	Stop()
	Close()
	TogglePause()

	// Seeking
	Seek(d time.Duration) error
	SeekYTDL(d time.Duration) error
	CancelSeekYTDL()

	// State queries
	IsPlaying() bool
	IsPaused() bool
	Drained() bool
	HasPreload() bool
	Seekable() bool
	IsStreamSeek() bool
	IsYTDLSeek() bool
	GaplessAdvanced() bool

	// Position and duration
	Position() time.Duration
	Duration() time.Duration
	PositionAndDuration() (time.Duration, time.Duration)

	// Volume
	SetVolumeMin(db float64)
	VolumeMin() float64
	SetVolume(db float64)
	Volume() float64

	// Speed
	SetSpeed(ratio float64)
	Speed() float64

	// Mono
	ToggleMono()
	Mono() bool

	// EQ
	SetEQBand(band int, dB float64)
	EQBands() [10]float64

	// Stream info
	StreamErr() error
	StreamTitle() string
	StreamBytes() (downloaded, total int64)

	// Audio samples for visualizer
	SamplesInto(dst []float64) int
	SampleRate() int
}

Engine is the interface used by the TUI model to control audio playback. It is satisfied by *Player and can be replaced with a mock for testing.

type Player

type Player struct {
	// contains filtered or unexported fields
}

Player is the audio engine managing the playback pipeline:

[Gapless] -> [10x Biquad EQ] -> [Volume] -> [Tap] -> [Ctrl] -> speaker
     ↑
     ├─ current: [Decode A] → [Resample A]
     └─ next:    [Decode B] → [Resample B]  (preloaded)

func New

func New(q Quality) (*Player, error)

New creates a Player and initializes the speaker with the given quality settings.

func (*Player) CancelSeekYTDL

func (p *Player) CancelSeekYTDL()

CancelSeekYTDL increments the seek generation, causing any in-flight SeekYTDL to discard its result instead of swapping streams.

func (*Player) ClearPreload

func (p *Player) ClearPreload()

ClearPreload discards the preloaded next track (e.g., when shuffle/repeat changes). Speaker is locked to ensure no in-flight gapless transition can reference the pipeline we're about to close.

func (*Player) Close

func (p *Player) Close()

Close fully stops the speaker and cleans up all resources.

func (*Player) Drained

func (p *Player) Drained() bool

Drained returns true if the current track ended with no preloaded next track.

func (*Player) Duration

func (p *Player) Duration() time.Duration

Duration returns the total duration of the current track. For seekable local files it is derived from the decoder's sample count. For HTTP streams where the decoder reports Len()==0, the metadata hint stored at pipeline build time (knownDuration) is returned instead.

func (*Player) EQBands

func (p *Player) EQBands() [10]float64

EQBands returns a copy of all 10 EQ band gains.

func (*Player) GaplessAdvanced

func (p *Player) GaplessAdvanced() bool

GaplessAdvanced returns true (once) when a gapless transition happened.

func (*Player) HasPreload

func (p *Player) HasPreload() bool

HasPreload returns true if a next track is already queued for gapless transition.

func (*Player) IsPaused

func (p *Player) IsPaused() bool

IsPaused returns true if playback is paused.

func (*Player) IsPlaying

func (p *Player) IsPlaying() bool

IsPlaying returns true if a track is loaded and playing (possibly paused).

func (*Player) IsStreamSeek

func (p *Player) IsStreamSeek() bool

IsStreamSeek reports whether the current track uses HTTP seek-by-reconnect. When true, Seek() releases the speaker lock during the HTTP request, so callers should dispatch seeks asynchronously to avoid blocking the UI.

func (*Player) IsYTDLSeek

func (p *Player) IsYTDLSeek() bool

IsYTDLSeek reports whether the current track uses yt-dlp seek-by-restart.

func (*Player) Mono

func (p *Player) Mono() bool

Mono returns true if mono output is enabled.

func (*Player) Play

func (p *Player) Play(path string, knownDuration time.Duration) error

Play opens and starts playing an audio file. On the first call it builds the long-lived EQ → volume → tap → ctrl chain and starts the speaker. Subsequent calls swap only the track source via the gapless streamer. knownDuration is the metadata duration (use 0 if unknown); it is used as a fallback when the decoder cannot determine the length (e.g. HTTP streams).

func (*Player) PlayYTDL

func (p *Player) PlayYTDL(pageURL string, knownDuration time.Duration) error

PlayYTDL starts playing a yt-dlp page URL via a piped yt-dlp | ffmpeg chain. Playback starts as soon as the first PCM samples arrive (~1-3s). Not seekable.

func (*Player) Position

func (p *Player) Position() time.Duration

Position returns the current playback position. For ranged HTTP streams (seek-by-reconnect), streamOffset is added to the decoder's sample-based position so the reported time is absolute within the track, not relative to the reconnect point.

func (*Player) PositionAndDuration

func (p *Player) PositionAndDuration() (time.Duration, time.Duration)

PositionAndDuration returns both position and duration under a single speaker lock, avoiding two separate lock acquisitions per tick.

func (*Player) Preload

func (p *Player) Preload(path string, knownDuration time.Duration) error

Preload builds a pipeline for the next track and queues it for gapless transition. knownDuration is the metadata duration (use 0 if unknown).

func (*Player) PreloadYTDL

func (p *Player) PreloadYTDL(pageURL string, knownDuration time.Duration) error

PreloadYTDL builds a yt-dlp pipe pipeline and queues it for gapless transition.

func (*Player) RegisterBufferedURLMatcher

func (p *Player) RegisterBufferedURLMatcher(match func(string) bool)

RegisterBufferedURLMatcher registers a function that identifies HTTP URLs requiring the buffered download + ffmpeg pipeline (e.g. Subsonic stream endpoints). This replaces hardcoded URL pattern checks.

func (*Player) RegisterStreamMetadataResolver

func (p *Player) RegisterStreamMetadataResolver(r StreamMetadataResolver)

RegisterStreamMetadataResolver installs a resolver used to pull now-playing metadata for streams that lack inline ICY metadata. Pass nil to disable.

func (*Player) RegisterStreamerFactory

func (p *Player) RegisterStreamerFactory(scheme string, f StreamerFactory)

RegisterStreamerFactory registers a factory for a custom URI scheme prefix (e.g., "spotify:"). When buildPipeline encounters a path starting with this prefix, it calls the factory to create the decoder instead of the normal file/HTTP pipeline.

func (*Player) SampleRate

func (p *Player) SampleRate() int

SampleRate returns the output sample rate in Hz.

func (*Player) SamplesInto

func (p *Player) SamplesInto(dst []float64) int

SamplesInto copies the latest audio samples into dst, avoiding allocation. Returns the number of samples written.

func (*Player) Seek

func (p *Player) Seek(d time.Duration) error

Seek moves the playback position by the given duration (positive or negative). For seekable local files, the decoder's Seek method is used directly. For HTTP streams with a known Content-Length and duration (seekableStream), seek is implemented by reconnecting with a Range: bytes=N- header and rebuilding the decoder at the computed byte offset. This is known as seek-by-reconnect. Returns nil immediately for non-seekable streams (e.g., Icecast radio). The speaker lock is acquired first (outer), then p.mu briefly to snapshot the current pipeline, ensuring consistent lock ordering with the audio thread. Clears the preloaded next pipeline to prevent a stale gapless transition.

func (*Player) SeekYTDL

func (p *Player) SeekYTDL(d time.Duration) error

SeekYTDL seeks a yt-dlp stream by restarting the pipeline at the target position. Must NOT be called with the speaker lock held. If a newer seek is requested (via CancelSeekYTDL) while this one is building, the result is discarded.

func (*Player) Seekable

func (p *Player) Seekable() bool

Seekable reports whether the current track supports seeking. Returns true for local files (decoder-native seek) and for HTTP streams with a known Content-Length and duration (seek-by-reconnect).

func (*Player) SetEQBand

func (p *Player) SetEQBand(band int, dB float64)

SetEQBand sets a single EQ band's gain in dB, clamped to [-12, +12].

func (*Player) SetSpeed

func (p *Player) SetSpeed(ratio float64)

SetSpeed sets the playback speed ratio, clamped to [0.25, 2.0]. 1.0 is normal speed, 2.0 is double speed, etc.

func (*Player) SetVolume

func (p *Player) SetVolume(db float64)

SetVolume sets the volume in dB, clamped to [VolumeMin, +6].

func (*Player) SetVolumeMin

func (p *Player) SetVolumeMin(db float64)

SetVolumeMin sets the minimum volume floor in dB, clamped to [-90, 0]. If the current volume is below the new floor it is immediately raised to match.

func (*Player) Speed

func (p *Player) Speed() float64

Speed returns the current playback speed ratio.

func (*Player) Stop

func (p *Player) Stop()

Stop halts playback and releases resources. The speaker is suspended so the ALSA audio callback goroutine blocks (zero CPU) instead of streaming silence. Resume is called automatically on the next Play().

func (*Player) StreamBytes

func (p *Player) StreamBytes() (downloaded, total int64)

StreamBytes returns the bytes downloaded and total content length for the current HTTP stream. Returns (0, 0) for local files or when no counter exists.

func (*Player) StreamErr

func (p *Player) StreamErr() error

StreamErr returns the current streamer error, if any (e.g., connection drops).

func (*Player) StreamTitle

func (p *Player) StreamTitle() string

StreamTitle returns the current ICY stream title (e.g., "Artist - Song"). Returns "" when no ICY metadata has been received.

func (*Player) ToggleMono

func (p *Player) ToggleMono()

ToggleMono switches between stereo and mono (L+R downmix) output.

func (*Player) TogglePause

func (p *Player) TogglePause()

TogglePause toggles between paused and playing states. When pausing, the speaker is suspended to save CPU; when unpausing it is resumed so the audio callback drains the queued samples.

func (*Player) Volume

func (p *Player) Volume() float64

Volume returns the current volume in dB.

func (*Player) VolumeMin

func (p *Player) VolumeMin() float64

VolumeMin returns the current minimum volume floor in dB.

type Quality

type Quality struct {
	SampleRate      int // output sample rate in Hz (e.g. 44100, 48000)
	BufferMs        int // speaker buffer in milliseconds
	ResampleQuality int // beep resample quality factor (1–4)
	BitDepth        int // PCM bit depth for FFmpeg output: 16 or 32 (32 = lossless)
}

Quality holds configurable audio output parameters.

type StreamMetadataResolver

type StreamMetadataResolver func(streamURL string) (fetch func(ctx context.Context) (string, error), interval time.Duration, ok bool)

StreamMetadataResolver matches a stream URL to a now-playing fetcher for broadcasters that carry no inline ICY metadata (e.g. NTS, FIP) and instead publish the current track via a separate JSON API. It returns a fetch function, the poll interval, and ok=false when the URL is not recognized.

type StreamerFactory

type StreamerFactory func(uri string) (beep.StreamSeekCloser, beep.Format, time.Duration, error)

StreamerFactory creates a beep.StreamSeekCloser for a custom URI scheme (e.g., spotify:track:xxx). Returns the streamer, its format, the track duration, and any error.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL