Files
netneutrality/docs/music.md
T

4.4 KiB

Music

Package: me.bobert.netneutrality.feature.music Engine: Lavaplayer 2.2.6 + the Lavalink YouTube source 1.18.2.

The music subsystem provides per-guild audio playback: searching/loading tracks from multiple sources, queueing, scheduling, and streaming audio into a Discord voice channel.

Key classes

Class Responsibility
MusicManager Central per-guild coordinator; owns the AudioPlayer and TrackScheduler.
QueueManager Holds the guild's track queue (LinkedBlockingDeque<TrackInfo>); enqueue/skip/shuffle.
TrackScheduler AudioEventAdapter; reacts to Lavaplayer events (onTrackEnd, onTrackStuck).
TrackLoadingHandler AudioLoadResultHandler; handles track/playlist/no-match/failure results.
TrackInfo Value object pairing an AudioTrack with its requester (Member).
SourceFinder Identifies the source/type of a query (YouTube, Udio, HTTP, …).
AudioPlayerSendHandler Bridges Lavaplayer output to JDA's AudioSendHandler.
source/UdioAudioSourceManager Custom Lavaplayer source manager for udio.com.
source/UdioAudioTrack Udio track; delegates playback to Mp3AudioTrack.

MusicManager keeps a static Map<String, MusicManager> of managers keyed by guild id, so each guild has its own player, scheduler, and queue.

Startup

MusicManager.initPlayerManager() (called from App.startup()) creates the Lavaplayer AudioPlayerManager and registers the source managers (YouTube, SoundCloud, the custom Udio source, and HTTP).

Audio pipeline (end-to-end)

/play query
   │
   ▼
MusicManager.play(query, ctx)
   │  SourceFinder identifies the source
   ▼
audioPlayerManager.loadItemOrdered(guild, identifier, new TrackLoadingHandler(...))
   │
   ├─ trackLoaded ──▶ connect to voice (MusicUtils) ──▶ QueueManager.queue(track, member)
   ├─ playlistLoaded / searchResults ──▶ install a LoadPlaylistTask (choice buttons)
   ├─ noMatches ──▶ reply "nothing found"
   └─ loadFailed ──▶ reply / log error
   │
   ▼
QueueManager: if the player is idle, start immediately; else append to the deque
   │
   ▼
AudioPlayerSendHandler streams PCM/Opus frames to the voice channel
   │
   ▼ (track ends)
TrackScheduler.onTrackEnd ──▶ QueueManager.startNextTrack()
  • Requester tracking — each queued item is a TrackInfo (track + requesting Member), used for now-playing/queue displays and vote-skip.
  • Playlists / search — playlist and multi-result loads produce a LoadPlaylistTask with per-result buttons (LoadTrackFromPlaylistTask), tying the music and task subsystems together. Playlist size is bounded by the MUSIC_MAXPLAYLISTSIZE guild setting.
  • Stuck tracks — TrackScheduler.onTrackStuck attempts recovery (reload/skip).
  • Output — playback messages are posted through a MusicChannelContext so they land in the guild's configured music channel.

Custom Udio source

UdioAudioSourceManager scrapes udio.com track pages: it extracts track metadata (title, artist, duration) and a direct playback URL from the Next.js payload (self.__next_f.push(...) calls) in the page HTML. Because Udio playback URLs expire, UdioAudioTrack re-fetches the URL during the process phase of the pipeline before streaming, delegating the actual decode/playback to Lavaplayer's Mp3AudioTrack.

Guild lifecycle & cleanup

  • MusicPlayerAutoDisconnect (scheduled task) leaves voice when the player has been idle.
  • MusicManagerCleanup (scheduled task) evicts unused per-guild MusicManager instances from the static map.
  • On shutdown, MusicManager.disconnectPlayers() stops all players and disconnects from voice (called from App.shutdown()).

All music commands extend MusicCommand (guild only + music role/channel enforced): PlayCommand, SkipCommand/VoteSkipCommand, QueueCommand/ViewQueueCommand, ClearQueueCommand, ShuffleCommand, JoinCommand/LeaveCommand, CurrentlyPlayingCommand, ResumeCommand/StopCommand, VolumeCommand, BitconnectCommand. See commands.md.