Melodix

music

Use Melodix’s Discord-independent Go engine to resolve tracks, manage a queue and deliver 20 ms Opus packets to a speaker or a custom sink. YouTube’s compatible Opus streams pass through without re-encoding; other paths use FFmpeg.

Requirements

Use Go 1.26+. The speaker sink uses ebitengine/oto; on Linux its build needs a C compiler and ALSA development files. Install FFmpeg on PATH for SoundCloud, radio and transcoding fallbacks. YouTube live broadcasts require yt-dlp and Node, Deno or Bun. Native YouTube passthrough needs neither media program.

Install

go get github.com/keshon/melodix/pkg/music/...

Quick start

This complete program plays a supplied URL through the default speaker. Run it with a real track URL. It stays alive until you press Enter, and stops playback before closing the sink provider.

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
    "sync"

    "github.com/keshon/melodix/pkg/music/player"
    "github.com/keshon/melodix/pkg/music/resolve"
    "github.com/keshon/melodix/pkg/music/sink"
)

func main() {
    if len(os.Args) != 2 {
        log.Fatal("usage: player <track-url>")
    }
    provider := sink.NewSpeakerProvider()
    defer provider.Close()
    p := player.New(provider, resolve.New())
    defer p.Stop(true)

    done := make(chan struct{})
    var watcher sync.WaitGroup
    watcher.Add(1)
    go func() {
        defer watcher.Done()
        for {
            select {
            case <-done:
                return
            case status := <-p.PlayerStatus:
                fmt.Println(status)
            }
        }
    }()
    defer func() { close(done); watcher.Wait() }()

    if err := p.Enqueue(os.Args[1], "", ""); err != nil {
        log.Print(err)
        return
    }
    if err := p.PlayNext(""); err != nil {
        log.Print(err)
        return
    }
    fmt.Println("Press Enter to stop.")
    _, _ = bufio.NewReader(os.Stdin).ReadString('\n')
}

For an interactive example, see examples/clispeaker. That example has a smaller command set than the application in cmd/cli.

Lifecycle and status

Create one player per target. Use exactly one long-lived receiver for PlayerStatus; competing receivers take events from each other. Stop the player before releasing its sink provider. The example owns and joins its watcher; the application frontends provide their own long-lived status consumers.

PlayNext can start opening a stream before its first packet arrives. When integrating a UI, use the player options/hooks and the status-delivery contract to distinguish synchronous command results from later playback transitions. The Discord-specific message lifecycle is owned by its voice service, not this library.

Playback and recovery

The resolver produces metadata and an ordered parser list. The player queues tracks, opens a recovery stream and hands packets to the sink. A failed first read advances the parser chain; an early interruption can reopen the source. Voice transport recovery has a separate retry budget. Full flows and diagrams are maintained in Architecture.

The sink owns the read loop. After RecoveryStream.Start opens once, RequestReopen asks the reading goroutine to reopen; another goroutine must not mutate its run state. See ownership rules, especially rules 2 and 3, before implementing a parser or custom sink.

Optional playback layers

Direct library consumption starts with cache and read-ahead disabled. Configure stream.SetCache and stream.SetBufferAhead once, before playback. The application configures these separately: its read-ahead default is 30 seconds.

The cache stores complete tracks under content keys and checks disk before opening parsers; radio is not cached. The read-ahead buffer wraps recovery so its lead can cover a reconnect. Consume it through RecoveryStream.Packets(). The architecture section owns the layering explanation; API comments describe setup and storage format.

Key extension points

  • Implement player.Resolver for different input resolution.
  • Implement sources.Searcher for ranked results on a searchable source.
  • Implement sink.AudioSink and sink.Provider for another output. Their API comments specify read-loop, transport-failure and frame-protection obligations.
  • Implement parsers.Streamer.Open, returning parsers.Opened, and register the parser under its frozen key. See the extension checklist.

Parsers can report through package-level loggers; wire their SetLogger methods once at startup if diagnostics are needed. FFmpeg and yt-dlp default to PATH; their package path variables support explicit executable locations.

Documentation

  • player: queue and playback engine
  • resolve: input resolution
  • sink: output contracts and speaker implementation
  • sources: metadata sources and search
  • parsers: stream backends
  • stream: opening and recovery
  • cache: content-keyed disk cache
  • Architecture: system flows
  • Ownership: lifecycle contracts and enforcement limits

License

music is licensed under the MIT License.