Help us shape the future of Go! Take the 2026 Go Developer Survey

beepout

package module
v0.0.0-...-5fd1b18 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2026 License: MIT Imports: 6 Imported by: 0

README

beepout

beepout is a beep/speaker-style audio output for the github.com/gopxl/beep ecosystem, using github.com/gen2brain/malgo as the backend.

It works with both github.com/gopxl/beep v1 and github.com/gopxl/beep/v2 from a single module. The library does not import either beep package, relying instead on Go's structural typing.

Features

  • Compatible with beep v1 and v2
  • Drop-in replacement for beep/speaker
  • Powered by malgo (miniaudio) with configurable backend priority
  • Per-instance API via New(Config) and *Speaker methods
  • Volume and logging controls
  • Runtime backend reconfiguration
  • Supports Android (AAudio/OpenSL ES) and other malgo-supported platforms
  • Thread-safe API

Installation

go get github.com/ancientcatz/beepout

Requires Go 1.26+ with CGO enabled.

Usage

Package-level API (beep v1/v2 speaker-compatible)
package main

import (
	"os"

	"github.com/ancientcatz/beepout"
	"github.com/gopxl/beep/v2/mp3"
)

func main() {
	f, _ := os.Open("track.mp3")
	defer f.Close()

	streamer, format, _ := mp3.Decode(f)
	defer streamer.Close()

	beepout.Init(int(format.SampleRate), 2048)
	defer beepout.Close()

	beepout.PlayAndWait(streamer)
}

Init also accepts optional configuration:

beepout.Init(int(format.SampleRate), 2048,
	beepout.WithBackends(malgo.BackendAaudio, malgo.BackendOpensl),
	beepout.WithVolume(0.8),
)
defer beepout.Close()
Instance API (with backend priority)
package main

import (
	"os"

	beep "github.com/gopxl/beep/v2"
	"github.com/gopxl/beep/v2/mp3"

	"github.com/ancientcatz/beepout"
	"github.com/gen2brain/malgo"
)

func main() {
	f, _ := os.Open("track.mp3")
	defer f.Close()

	streamer, format, _ := mp3.Decode(f)
	defer streamer.Close()

	s, _ := beepout.New(beepout.Config{
		SampleRate: int(format.SampleRate),
		BufferSize: 2048,
		Backends: []malgo.Backend{
			malgo.BackendAaudio,     // Android (preferred)
			malgo.BackendOpensl,     // Android (fallback)
			malgo.BackendPulseaudio, // Linux
			malgo.BackendCoreaudio,  // macOS / iOS
			malgo.BackendWasapi,     // Windows
		},
	})
	defer s.Close()

	done := make(chan struct{})

	s.Play(beep.Seq(
		streamer,
		beep.Callback(func() {
			close(done)
		}),
	))

	<-done
}

To use beep v1, change the import paths from github.com/gopxl/beep/v2 to github.com/gopxl/beep. No other code changes are required.

API

Package-level API
func Init(sampleRate int, bufferSize int, opts ...InitOption) error
func Close()
func Lock()
func Unlock()
func Play(s ...Streamer)
func PlayAndWait(s ...Streamer)
func Suspend() error
func Resume() error
func Clear()
beepout-specific package-level API
type InitOption func(*initConfig)

func WithBackends(backends ...malgo.Backend) InitOption
func WithVolume(volume float64) InitOption
func WithLogProc(logProc malgo.LogProc) InitOption

func SetBackends(backends []malgo.Backend) error
func Reinit(cfg Config) error
func Backends() []malgo.Backend
func SampleRate() int
func BufferSize() int
Instance-level API
type Streamer interface {
	Stream(samples [][2]float64) (n int, ok bool)
	Err() error
}

type StreamerFunc func(samples [][2]float64) (n int, ok bool)

type Config struct {
	SampleRate int
	BufferSize int
	Backends   []malgo.Backend
	Volume     float64
	LogProc    malgo.LogProc
}

type Speaker struct { /* unexported fields */ }

func New(cfg Config) (*Speaker, error)

func (s *Speaker) Close() error
func (s *Speaker) Play(streamers ...Streamer)
func (s *Speaker) Clear()
func (s *Speaker) Pause()
func (s *Speaker) Resume()
func (s *Speaker) Lock()
func (s *Speaker) Unlock()
func (s *Speaker) SampleRate() int
func (s *Speaker) BufferSize() int
func (s *Speaker) Backends() []malgo.Backend

Streamer and StreamerFunc are structurally compatible with beep v1 and v2.

License

MIT. See LICENSE.

Documentation

Overview

Package beepout implements playback of audio streamers through the malgo (miniaudio) backend.

beepout is structurally compatible with both github.com/gopxl/beep and github.com/gopxl/beep/v2: it accepts any value whose Stream and Err methods match the well-known beep.Streamer signature, without importing either beep module from the library implementation. See the Streamer interface in this file for the exact contract.

Example (SineWave)

Example_sineWave generates a 440 Hz sine tone using beepout's own StreamerFunc type and plays it through the default backend. It does not depend on beep at all, demonstrating that the beepout API is usable standalone.

const sampleRate = 44100
const freq = 440.0

s, err := New(Config{
	SampleRate: sampleRate,
	BufferSize: 2048,
	Volume:     0.5,
})
if err != nil {
	panic(err)
}
defer s.Close()

// A self-contained infinite sine generator. The phase is tracked in
// the closure and wrapped with math.Modf so it stays in [0, 1).
dt := freq / float64(sampleRate)
var t float64
tone := StreamerFunc(func(samples [][2]float64) (int, bool) {
	for i := range samples {
		v := math.Sin(t * 2.0 * math.Pi)
		samples[i][0] = v
		samples[i][1] = v
		_, t = math.Modf(t + dt)
	}
	return len(samples), true
})

s.Play(tone)
<-time.After(2 * time.Second)
s.Clear()

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Backends

func Backends() []malgo.Backend

Backends returns the malgo backend priority list that the default Speaker was initialized with (via Init or Reinit). The returned slice is a fresh copy and may be safely modified by the caller.

Returns nil if no default Speaker is currently initialized, or if the default Speaker was initialized via Init (which uses malgo's platform default ordering, represented internally as nil).

Backends is beepout-specific: it has no equivalent in the beep v1/v2 speaker API.

func BufferSize

func BufferSize() int

BufferSize returns the device period size in frames that the default Speaker was initialized with. Returns 0 if no default Speaker is currently initialized.

BufferSize is beepout-specific: the beep v1/v2 speaker package does not expose a package-level BufferSize function.

func Clear

func Clear()

Clear removes all queued streamers from the default Speaker. Samples already buffered by the device may still be played.

Clear must not be called while the caller holds the speaker lock acquired via Lock — that would deadlock.

func Close

func Close()

Close closes the package-level default Speaker and releases all of its resources. It is safe to call Close multiple times; subsequent calls are no-ops. After Close returns, the default Speaker must be reinitialized with Init before any other package-level function can be used again.

Close does not return an error, matching the beep v1 and v2 speaker.Close() signature. If you need the close error, use a *Speaker created with New and call its Close method directly.

func Init

func Init(sampleRate int, bufferSize int, opts ...InitOption) error

Init initializes the package-level default Speaker with the given sample rate and buffer size, plus optional configuration applied via InitOption values (such as WithBackends, WithVolume, WithLogProc).

The original two-argument form is preserved for backwards compatibility:

beepout.Init(48000, 2048)

passes nil backends (malgo's platform default) and a neutral volume of 1.0. The variadic form allows additional configuration without requiring callers to construct a full Config:

beepout.Init(48000, 2048,
    beepout.WithBackends(malgo.BackendAaudio, malgo.BackendOpensl),
    beepout.WithVolume(0.8))

Init must be called once before any other package-level function (other than Close) can be used. Init returns an error if the default Speaker has already been initialized; call Close first to reinitialize.

sampleRate is in frames per second (e.g. 44100, 48000). bufferSize is the device period size in frames (e.g. 256, 2048); larger values reduce CPU usage, smaller values reduce latency.

On failure, Init does not modify the default Speaker state: if a default Speaker was already initialized, it remains in place; if no default Speaker was initialized, it remains nil.

func Lock

func Lock()

Lock acquires the default Speaker's mutex. While held, the audio callback will not advance any queued streamer, making it safe to mutate the state of a streamer that has already been added via Play.

Lock must be paired with Unlock. Hold the lock for as short a time as possible — the audio callback blocks while the lock is held, and holding it for longer than one device period will cause an underrun.

Do not call Play, PlayAndWait, Clear, Suspend, Resume, Init, or Close while holding the lock: those functions acquire the same mutex internally and would deadlock. The intended pattern is to Lock, mutate streamer state directly, then Unlock.

Lock must only be called after Init has succeeded; calling Lock before Init is a programming error and Unlock will panic when it tries to release a mutex that was never acquired.

func Play

func Play(streamers ...Streamer)

Play adds the given streamers to the default Speaker's playback queue. Streamers are mixed additively; drained streamers are removed automatically by the audio callback.

Play must not be called while the caller already holds the speaker lock acquired via Lock — that would deadlock. See Lock for details.

func PlayAndWait

func PlayAndWait(streamers ...Streamer)

PlayAndWait plays the given streamers through the default Speaker and blocks until all of them have drained. It then sleeps for the duration of one buffer period to allow any samples already in the device buffer to be played, mirroring the behavior of beep v1/v2 speaker.PlayAndWait.

Like Play, PlayAndWait must not be called while the caller holds the speaker lock.

Each streamer is wrapped internally so that its drain signal is observable from the calling goroutine. The wrapper satisfies beepout.Streamer (and therefore beep.Streamer from both v1 and v2) without altering the original streamer's samples or error.

func Reinit

func Reinit(cfg Config) error

Reinit re-initializes the default Speaker with the given Config, replacing the previous one entirely.

Reinit is more flexible than SetBackends: it allows changing the sample rate, buffer size, volume, backend priority list, and log callback in a single call. Use SetBackends if only the backend priority list needs to change, or use the variadic Init with options (WithBackends, WithVolume, WithLogProc) for the initial configuration.

Reinit is atomic with respect to failure: the new Speaker is constructed WITHOUT closing the current one. If the new Speaker cannot be created, the current default Speaker is left untouched and Reinit returns an error. Callers can rely on the default Speaker remaining usable after a failed Reinit.

On success, the old default Speaker is closed and replaced atomically: no callers observe a moment with no default Speaker. Any streamers that were queued via Play are lost when the old Speaker is closed; callers should re-add them after Reinit returns successfully.

Reinit returns an error if no default Speaker has been initialized (call Init first) or if the new Speaker cannot be created.

Reinit must not be called concurrently with Init, Close, or another SetBackends/Reinit; lifecycle operations on the default Speaker are not serialized against each other.

Reinit is beepout-specific: it has no equivalent in the beep v1/v2 speaker API.

func Resume

func Resume() error

Resume restarts the default Speaker's playback device after a prior Suspend. If the device was not suspended, Resume is a no-op.

Resume is independent of the Speaker.Resume method, which undoes a Speaker.Pause call. The two address different layers: Speaker.Pause and Speaker.Resume toggle a flag in the mixer; Suspend and Resume toggle the device's start/stop state.

Resume must not be called while the caller holds the speaker lock acquired via Lock — that would deadlock.

func SampleRate

func SampleRate() int

SampleRate returns the playback sample rate in frames per second that the default Speaker was initialized with. Returns 0 if no default Speaker is currently initialized.

SampleRate is beepout-specific: the beep v1/v2 speaker package does not expose a package-level SampleRate function. It is provided here for symmetry with the (*Speaker).SampleRate method.

func SetBackends

func SetBackends(backends []malgo.Backend) error

SetBackends re-initializes the default Speaker with the given malgo backend priority list, preserving the sample rate, buffer size, and volume that were configured by the previous Init or Reinit call.

SetBackends is atomic with respect to failure: the new Speaker is constructed WITHOUT closing the current one. If the new Speaker cannot be created (for example because none of the requested backends are available on this platform), the current default Speaker is left untouched and SetBackends returns an error. Callers can rely on the default Speaker remaining usable after a failed SetBackends.

On success, the old default Speaker is closed and replaced atomically: no callers observe a moment with no default Speaker. Any streamers that were queued via Play are lost when the old Speaker is closed; callers should re-add them after SetBackends returns successfully.

SetBackends returns an error if no default Speaker has been initialized (call Init first) or if the new Speaker cannot be created.

SetBackends must not be called concurrently with Init, Close, or another SetBackends/Reinit; lifecycle operations on the default Speaker are not serialized against each other.

SetBackends is beepout-specific: it has no equivalent in the beep v1/v2 speaker API, which only allows backends to be configured at Init time.

func Suspend

func Suspend() error

Suspend suspends audio playback on the default Speaker by stopping the underlying malgo device. The device is not uninitialized; use Resume to restart it.

Suspend is independent of the Speaker.Pause method, which uses a flag-based approach to silence the output without stopping the device. Suspend saves more power but has higher resume latency than Pause, and is intended for situations where no audio will be played for an extended period.

Suspend must not be called while the caller holds the speaker lock acquired via Lock — that would deadlock. Suspend also must not run concurrently with Close on the default speaker; the two operations both touch the underlying malgo device and are not synchronized against each other (matching the same constraint in beep v1/v2's speaker package).

func Unlock

func Unlock()

Unlock releases the default Speaker's mutex previously acquired by Lock.

Unlock must only be called after a successful Lock. Calling Unlock without a prior Lock (or after the default Speaker has been reinitialized by Init between Lock and Unlock) is a programming error and may panic.

Types

type Config

type Config struct {
	// SampleRate is the playback sample rate in frames per second. Must be
	// positive. Typical values are 44100, 48000, etc.
	SampleRate int

	// BufferSize is the device period size in frames. Larger values reduce
	// CPU usage and the risk of underruns; smaller values reduce latency.
	// Must be positive. Common values range from 256 to 4096.
	BufferSize int

	// Backends is an ordered list of preferred malgo backends. The first
	// backend that initializes successfully is used. If empty, malgo's
	// platform default backend order applies.
	//
	// On Android, callers should pass:
	//
	//      []malgo.Backend{malgo.BackendAaudio, malgo.BackendOpensl}
	//
	// so that AAudio is preferred (lower latency, better features) and
	// OpenSL ES is used as a fallback on older devices.
	Backends []malgo.Backend

	// Volume is a linear gain applied to every mixed sample before it is
	// written to the device. 1.0 leaves the signal unchanged, 0.0 mutes
	// playback, 0.5 halves the amplitude. The zero value defaults to 1.0
	// at construction time, so callers that want true silence must set
	// Volume explicitly to a very small positive number.
	Volume float64

	// LogProc, if non-nil, is invoked by malgo with diagnostic messages.
	// It may be called from any goroutine; callers are responsible for any
	// synchronization required by their logging implementation.
	LogProc malgo.LogProc
}

Config configures a Speaker at construction time. The zero value is not usable; SampleRate and BufferSize must be set explicitly.

type InitOption

type InitOption func(*initConfig)

InitOption is an optional configuration argument for the package-level Init function. An InitOption mutates an internal config builder; it is applied left-to-right, with later options overriding earlier ones for the same field.

InitOption values are constructed by the WithBackends, WithVolume, and WithLogProc helper functions. Callers may also define their own InitOption values for use cases such as conditional configuration based on host platform detection.

InitOption is beepout-specific: the beep v1/v2 speaker package has no options API because it does not expose backend configuration at all.

func WithBackends

func WithBackends(backends ...malgo.Backend) InitOption

WithBackends specifies the malgo backend priority list for the default Speaker. malgo tries each backend in order and uses the first one that initializes successfully on the host platform.

If WithBackends is not passed to Init (or is passed with no arguments), Init uses malgo's platform default backend ordering.

On Android, callers should pass:

beepout.Init(48000, 2048,
    beepout.WithBackends(malgo.BackendAaudio, malgo.BackendOpensl))

so that AAudio is preferred (lower latency, better features) and OpenSL|ES is used as a fallback on older devices.

WithBackends copies the caller's slice; external mutation of the argument after WithBackends returns does not affect the configured priority list.

WithBackends is beepout-specific.

func WithLogProc

func WithLogProc(logProc malgo.LogProc) InitOption

WithLogProc specifies an optional malgo logging callback. malgo invokes the callback from arbitrary goroutines; callers are responsible for any synchronization required by their logging implementation.

If WithLogProc is not passed to Init, no logging callback is registered.

WithLogProc is beepout-specific.

func WithVolume

func WithVolume(volume float64) InitOption

WithVolume specifies the linear gain applied to every mixed sample before it is written to the device. 1.0 leaves the signal unchanged, 0.5 halves the amplitude, 0.0 is treated as the default (1.0) for Config-compatibility — callers that want true digital silence should pass a very small positive number such as 1e-10.

If WithVolume is not passed to Init, the default Speaker uses a neutral volume of 1.0.

WithVolume is beepout-specific.

type Speaker

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

Speaker is a malgo-backed audio sink that plays Streamer values. A Speaker owns a malgo context and a single playback device. Speaker methods are safe to call from multiple goroutines concurrently.

After Close returns, the Speaker is no longer usable. All public methods become no-ops (Close remains safe to call any number of times).

func New

func New(cfg Config) (*Speaker, error)

New creates and starts a Speaker.

New initializes a malgo context using the configured backend priority, creates a stereo Float32 playback device with the requested sample rate and period size, and starts the device automatically. The returned Speaker is ready to accept streamers via Play.

If New returns an error, no resources are leaked: any partially initialized malgo state is cleaned up before returning.

func (*Speaker) Backends

func (s *Speaker) Backends() []malgo.Backend

Backends returns the malgo backend priority list that the Speaker was constructed with (a copy of Config.Backends at construction time). The returned slice is a fresh copy and may be safely modified by the caller.

If Config.Backends was nil (the platform default) at construction time, Backends returns nil.

func (*Speaker) BufferSize

func (s *Speaker) BufferSize() int

BufferSize returns the device period size in frames that the Speaker was constructed with. The value is taken from Config.BufferSize at construction time and does not change for the lifetime of the Speaker.

func (*Speaker) Clear

func (s *Speaker) Clear()

Clear removes all queued streamers. Samples already buffered by the device may still be played.

func (*Speaker) Close

func (s *Speaker) Close() error

Close stops the playback device, uninitializes it, uninitializes the malgo context, and frees all associated resources.

Close is idempotent: calling it multiple times is safe and returns the error from the first invocation on every subsequent call.

After Close returns, every other Speaker method becomes a no-op.

func (*Speaker) Lock

func (s *Speaker) Lock()

Lock acquires the speaker mutex. While held, the audio callback will not advance any queued streamer, which makes it safe to mutate the state of a streamer that has already been added via Play.

Lock must be paired with Unlock. Hold the lock for as short a time as possible — the audio callback blocks while the lock is held, and holding it for longer than one device period will cause an underrun.

Do not call Play, Clear, Pause, Resume, or Close while holding the lock: those methods acquire the same mutex internally and would deadlock. The intended pattern is to Lock, mutate streamer state directly, then Unlock.

func (*Speaker) Pause

func (s *Speaker) Pause()

Pause stops emitting audio from queued streamers without stopping the device. While paused, the audio callback outputs silence and queued streamers are not advanced, so playback resumes from the same position.

Pause is independent from malgo's device start/stop state; the device keeps running so that Resume can take effect with the lowest possible latency. To save power when no audio is playing for an extended period, consider calling Close instead.

func (*Speaker) Play

func (s *Speaker) Play(streamers ...Streamer)

Play adds the given streamers to the playback queue. Streamers are mixed together additively. Drained streamers are removed from the queue automatically by the audio callback.

Play is safe to call from multiple goroutines, but must not be called while the caller already holds the speaker lock acquired via Lock — that would deadlock. See Lock for details.

func (*Speaker) Resume

func (s *Speaker) Resume()

Resume undoes a prior Pause. If the speaker was not paused, Resume is a no-op.

func (*Speaker) SampleRate

func (s *Speaker) SampleRate() int

SampleRate returns the playback sample rate in frames per second. The value is taken from Config.SampleRate at construction time and does not change for the lifetime of the Speaker.

func (*Speaker) Unlock

func (s *Speaker) Unlock()

Unlock releases the speaker mutex previously acquired by Lock.

type Streamer

type Streamer interface {
	Stream(samples [][2]float64) (n int, ok bool)
	Err() error
}

Streamer is the minimal contract required by beepout for an audio source.

The signature is identical to the Streamer interface exposed by both github.com/gopxl/beep and github.com/gopxl/beep/v2, so values of type beep.Streamer from either version are accepted transparently by Play and any other beepout API that takes a Streamer. Future beep releases that preserve this signature will continue to work without code changes.

Stream copies at most len(samples) next stereo frames into samples. Each frame is a [2]float64 where index 0 is the left channel and index 1 is the right channel. Sample values are linear PCM in the range [-1, 1].

Stream returns the number of frames streamed and a boolean ok that reports whether more samples may follow. The three valid return patterns are:

  1. n == len(samples) && ok — the streamer produced all requested frames.
  2. 0 < n && n < len(samples) && ok — the streamer is now drained; only case 3 may follow.
  3. n == 0 && !ok — the streamer is exhausted permanently.

Err returns a non-nil error if streaming failed. When Err returns a non-nil error, Stream must return 0, false forever afterwards.

type StreamerFunc

type StreamerFunc func(samples [][2]float64) (n int, ok bool)

StreamerFunc adapts an ordinary function into a Streamer. It is the structural twin of beep.StreamerFunc from both beep v1 and v2, so a value of type beep.StreamerFunc satisfies beepout.Streamer directly.

Example:

tone := beepout.StreamerFunc(func(samples [][2]float64) (int, bool) {
    for i := range samples {
        samples[i] = [2]float64{0.5, 0.5}
    }
    return len(samples), true
})
speaker.Play(tone)

func (StreamerFunc) Err

func (sf StreamerFunc) Err() error

Err always returns nil. StreamerFunc has no error state.

func (StreamerFunc) Stream

func (sf StreamerFunc) Stream(samples [][2]float64) (n int, ok bool)

Stream calls the wrapped function.

Jump to

Keyboard shortcuts

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