beep

repository·main·Indexed 20 days ago

https://github.com/gopxl/beep

A lightweight Go library for audio playback and processing. Beep uses a unified Streamer interface to decode audio formats including WAV, MP3, Ogg Vorbis, FLAC, and MIDI, and supports real-time effects, mixing, sequencing, and looping. It focuses exclusively on stereo (two-channel) audio and provides tools for managing audio data via Buffers and the speaker package for output.

Tokens
10.2K
Snippets
42
Records
55
Agent score
68%

What's inside beep

  1. Understand the Doppler Stereo Room example

    main

    The Doppler Stereo Room is a demonstration of 3D sound localization using the beep library. It simulates a 3D environment where sound positioning is achieved through interaural time delay (delaying sound in one ear relative to the other) rather than volume attenuation.

    Key concepts of this simulation:

    • Spatial Representation: The user's head is represented by a black square. Speakers are represented by colored squares.
    • Stereo Mapping: The green speaker corresponds to the left stereo channel, and the blue speaker corresponds to the right stereo channel.
    • 3D Effect Mechanism: The effect is achieved by delaying the sound in one ear to simulate sound localization. Volume remains constant in both ears regardless of position.
  2. What is a Streamer and how to implement one

    main

    A Streamer is the core interface in Beep used to produce audio. To create a custom streamer, you must implement the Streamer interface, which consists of two methods:

    1. Stream(samples [][2]float64) (n int, ok bool): This method fills the provided slice of stereo samples (where each sample is [2]float64). It returns n (the number of samples filled) and ok (a boolean indicating if the streamer has more data; false means it is drained).
    2. Err() error: This method returns any error encountered during streaming.

    Design Note: Error handling is separated from the Stream method to prevent a single faulty streamer from crashing an entire audio pipeline (like a beep.Mixer). This allows the pipeline to continue playing other streamers while the error is caught via the Err() method.

    type Streamer interface {
        Stream(samples [][2]float64) (n int, ok bool)
        Err() error
    }
  3. What is a Streamer?

    main

    A Streamer is an interface that acts as an io.Reader for audio samples.

    • Audio Samples: In Beep, samples are represented as [2]float64, where the first float is for the left speaker and the second is for the right speaker.
    • Stateful: A Streamer is stateful (like an audio tape). Once you stream it, it 'drains'. If you need to play it again, you must use a beep.StreamSeeker to rewind it.
    • The Stream Method: The core method is Stream(samples [][2]float64) (n int, ok bool), which fills a slice of samples and returns how many were filled and whether the stream is still active.
  4. How the Streamer interface works

    main

    Beep is built around the Streamer interface. This interface is conceptually similar to io.Reader, but designed specifically for audio data.

    Because everything in Beep is a Streamer, you can easily:

    • Compose sounds: Use compositors to loop, mix, sequence, or pause/resume audio.
    • Apply effects: Wrap a Streamer with an effect (like volume or playback speed changes) to create a new Streamer.
    • Generate audio: Implement the Streamer interface yourself to generate artificial sounds programmatically.

    Note: Beep simplifies its architecture by focusing exclusively on stereo (two-channel) audio.

  5. How streamers compose together

    main

    Beep uses a decorator pattern where streamers are wrapped inside other streamers to add functionality. This allows for complex audio processing chains (often called "DJ panels").

    Composition Example: To create a playback chain that loops indefinitely, allows pausing, and applies a volume effect, you wrap them in layers:

    1. Start with the base streamer.
    2. Wrap in beep.Loop(-1, streamer) to handle looping.
    3. Wrap in &beep.Ctrl{...} to handle pausing.
    4. Wrap in &effects.Volume{...} to handle volume.
    5. Wrap in beep.ResampleRatio(...) to handle speed.

    Each layer only requests the samples it needs from the layer below, making this approach highly efficient and modular.

    // A complex composition chain
    loop := beep.Loop(-1, streamer)
    ctrl := &beep.Ctrl{Streamer: loop, Paused: false}
    vol := &effects.Volume{Streamer: ctrl, Base: 2, Volume: 0}
    speedy := beep.ResampleRatio(4, 1, vol)
    
    speaker.Play(speedy)
  6. Load audio into memory using beep.Buffer

    main

    To avoid the overhead of streaming from disk and to allow multiple simultaneous instances of the same sound (e.g., sound effects like gunshots), use beep.Buffer.

    beep.Buffer stores samples encoded as bytes to save space, so it requires a beep.Format during initialization. You can populate the buffer using buffer.Append(streamer), which is a blocking call that drains the provided streamer into the buffer. Once appended, the original streamer can be closed.

    When to use a Buffer

    • For small files.
    • For sounds that need to be played many times.
    • For sounds that need to overlap (multiple instances playing at once).

    When to stream from disk

    • For very large files.
    • When you only need to play one instance at a time.
    • To minimize memory usage and startup time.
    // 1. Decode the file
    streamer, format, err := mp3.Decode(f)
    if err != nil {
    	log.Fatal(err)
    }
    
    // 2. Initialize the buffer with the audio format
    buffer := beep.NewBuffer(format)
    
    // 3. Append the streamer to the buffer (this is blocking)
    buffer.Append(streamer)
    
    // 4. Close the original streamer as it is now drained
    streamer.Close()
  7. Track the current position of a StreamSeeker

    main

    If a streamer implements the beep.StreamSeeker interface, you can track its playback position using Position() int. Because Position() returns the number of samples rather than a time duration, you must use the SampleRate.D() method from your audio format to convert the sample count into a time.Duration.

    Important: When accessing an active streamer from a background loop (e.g., to print status), you must wrap the access with speaker.Lock() and speaker.Unlock() to prevent race conditions with the speaker's background processing.

    // Assuming 'streamer' is a beep.StreamSeeker and 'format' is the audio format
    speaker.Lock()
    position := format.SampleRate.D(streamer.Position())
    fmt.Println(position.Round(time.Second))
    speaker.Unlock()
  8. Implement a dynamic Queue streamer

    main

    To create a dynamic queue that plays streamers one after another, implement a struct that holds a slice of beep.Streamer.

    When implementing the Stream method for a queue:

    1. If the queue is empty, fill the requested samples with silence (zeros).
    2. If the queue has streamers, call Stream on the first streamer in the slice.
    3. If the first streamer returns ok == false, it is drained; remove it from the slice and continue to the next streamer until the requested number of samples is filled.

    Important: When adding streamers to a queue that is currently being played by the speaker, wrap the Add call with speaker.Lock() and speaker.Unlock() to ensure thread safety.

    type Queue struct {
    	streamers []beep.Streamer
    }
    
    func (q *Queue) Add(streamers ...beep.Streamer) {
    	q.streamers = append(q.streamers, streamers...)
    }
    
    func (q *Queue) Stream(samples [][2]float64) (n int, ok bool) {
    	filled := 0
    	for filled < len(samples) {
    		if len(q.streamers) == 0 {
    			for i := range samples[filled:] {
    				samples[i][0] = 0
    				samples[i][1] = 0
    			}
    			break
    		}
    
    		n, ok := q.streamers[0].Stream(samples[filled:])
    		if !ok {
    			q.streamers = q.streamers[1:]
    		}
    		filled += n
    	}
    	return len(samples), true
    }
    
    func (q *Queue) Err() error {
    	return nil
    }
  9. Decode audio files with format-specific packages

    main

    To play an audio file, you must first open it using the standard os.Open and then decode it using the appropriate package for the file format.

    Common decoders include:

    • github.com/gopxl/beep/mp3 for MP3 files
    • github.com/gopxl/beep/wav for WAV files
    • (Other formats like OGG or FLAC follow a similar pattern)

    Important: Decoders like mp3.Decode return a streamer that performs on-line decoding. This means the file is read from disk as needed, allowing you to stream very large files with minimal RAM usage. Because of this, do not close the underlying file f manually; instead, call streamer.Close() to ensure the file is handled correctly when playback is finished.

    // Example decoding an MP3
    f, err := os.Open("song.mp3")
    if err != nil {
    	log.Fatal(err)
    }
    
    streamer, format, err := mp3.Decode(f)
    if err != nil {
    	log.Fatal(err)
    }
    defer streamer.Close()
  10. Play audio asynchronously with speaker.Play

    main

    The speaker.Play(streamer) function is asynchronous. It starts playing the streamer in the background and returns immediately. If your main function exits immediately after calling Play, the program will terminate before any sound is heard.

    To wait for a streamer to finish, you can use beep.Seq to chain the audio streamer with a beep.Callback that signals a channel when finished.

    done := make(chan bool)
    speaker.Play(beep.Seq(streamer, beep.Callback(func() {
    	done <- true
    })))
    
    <-done // Wait for the song to finish