Skip to content

Queue Playback

manish singh edited this page Jul 25, 2026 · 1 revision

Queue Playback

QueuePlayer is the main entry point for products that want Veloura's PCM output without importing Discord. PCMQueuePlayer remains an alias for backward compatibility.

Create tracks

from veloura.audio import AudioTrack

track = AudioTrack.from_source(
    "./music/song.flac",
    title="Song Title",
    duration=213.4,
    artist="Artist",
    album="Album",
    artwork_url="https://example.com/cover.jpg",
    source_id="catalogue:track:123",
    mood="late-night",
)

Common fields:

Field Meaning
stream_url Local path or directly playable media URL passed to FFmpeg.
webpage Human-facing source page, if different from the media URL.
duration Source duration in seconds. Accurate duration enables correctly timed crossfades.
source_id Stable application identifier used before URL or title for cache identity.
payload Application-owned object; Veloura does not inspect it.
trim_start / trim_end Seconds removed from the beginning or end.
gain Linear gain multiplier, clamped during playback.
tempo Playback speed ratio used by FFmpeg.
crossfade_seconds Optional per-track cap for the outgoing transition.
analysis Veloura's serializable preparation and planning details.

Extra keyword arguments passed to from_source() are placed in track.metadata.

Create a bounded player

from veloura.audio import QueuePlayer

player = QueuePlayer(
    volume=0.65,
    crossfade_seconds=5.0,
    max_queue_size=100,
)

For any queue exposed to users, set max_queue_size. An enqueue beyond that limit raises OverflowError, allowing your API or bot to return a clear error.

Queue controls

player.enqueue(track)
player.extend([second_track, third_track])

player.set_volume(0.8)       # clamped to 0.0 through 1.5
player.set_crossfade(6.0)    # clamped to 0 through 15 seconds

current = player.current_track()
up_next = player.next_track()
queued = player.queued_tracks()

player.skip()
player.clear()               # keeps the current track, clears pending tracks
player.stop()                # stops playback and clears everything

skip() returns whether Veloura had something to skip or promote.

Consume PCM frames

while player.is_active():
    frame = player.read_frame()
    if not frame:
        break
    output.write(frame)

Each non-empty frame represents 20 ms of 48 kHz stereo signed 16-bit PCM and is 3,840 bytes. A final read returns empty bytes. A temporary decoder stall can produce a complete silence frame while Veloura waits for more PCM.

Only one playback consumer should call read_frame(). Queue controls are protected by the underlying source lock and can be called from control paths, but your application should still keep ownership of its higher-level state.

Understand crossfade timing

The effective fade is the smallest applicable limit:

  • The player's configured crossfade.
  • The outgoing track's crossfade_seconds, when prepared.
  • One third of the outgoing playable duration.
  • The hard realtime maximum of 15 seconds.

When the remaining duration reaches that value, Veloura opens the next stream and mixes both frames using cosine/sine equal-power gains. This avoids the obvious volume dip produced by a simple linear fade.

Reliable duration metadata matters. If a live stream has no known duration, Veloura cannot schedule an ending crossfade and will move on when FFmpeg reaches the end.

Observe playback

snapshot = player.snapshot()

print(snapshot.current)
print(snapshot.elapsed)
print(snapshot.duration)
print(snapshot.queue)
print(snapshot.volume)
print(snapshot.crossfade_seconds)
print(snapshot.error)
print(snapshot.busy)

snapshot() is intentionally cheap and non-blocking. If the mixer lock is busy, the snapshot can report busy=True and return the best immediately available view.

Always expose snapshot.error in logs or diagnostics. Invalid files, expired stream URLs, and decoder failures are recorded there instead of being silently lost.

Prepare the current pair

from veloura.audio import transition_preset

plan = player.prepare_next_transition_pair(
    transition_preset("automix"),
    analysis_seconds=45,
    timeout=8,
)

if plan:
    print(plan.crossfade_seconds, plan.reason, plan.confidence)

This analyzes the current and next tracks and mutates their transition fields. It returns None until both tracks are available. Run it away from a strict audio callback because beat analysis invokes FFmpeg.

Clone this wiki locally