oto

repository·main·Indexed 24 days ago

https://github.com/ebitengine/oto

A low-level audio library for Go that provides a simple interface to play sounds across multiple platforms, including Windows, macOS, Linux, Android, iOS, and WebAssembly. It utilizes a Context to manage audio drivers and Players to consume audio bytes from io.Reader sources.

Tokens
2.9K
Snippets
5
Records
20
Agent score
84%

What's inside oto

  1. Overview of Oboe for Android Audio

    main

    Oboe is a C++ library designed for building high-performance audio applications on Android. It provides a simplified, unified API that works across multiple Android API levels, starting from API level 16 (Jelly Bean).

    Key features include:

    • Broad Compatibility: Runs on 99% of Android devices (API 16+).
    • Automatic API Selection: Automatically chooses between OpenSL ES (API 16+) and AAudio (API 27+) to ensure the best possible performance on the target device.
    • Latency Management: Includes automatic latency tuning to minimize audio delay.
    • Modern C++: Designed for clean and elegant code using modern C++ standards.
  2. How Oto (v3) works: Contexts and Players

    main

    Oto (v3) uses two main components to manage audio:

    1. Context: Handles interactions with the OS and audio drivers. You should create exactly one context per program. The context configuration (sample rate, channels, format) cannot be changed after creation.
    2. Players: Created from a Context. Each player is given an io.Reader that provides the audio bytes.

    Key Constraints:

    • A single io.Reader must not be shared by multiple players.
    • When creating a context, you must wait for the readyChan to signal that the hardware audio devices are ready before playing sounds.
    • Player.Play() is asynchronous; it starts playback and returns immediately.
  3. Play sounds by file streaming

    main

    To avoid loading large audio files entirely into memory, you can stream them directly from a file using os.Open. The io.Reader provided to the player will read from the file as needed.

    Important: You must keep the file object alive for the entire duration of playback. If the file is closed before the player finishes reading, you may hear static. You may need to store a reference to the file in a struct to ensure it stays in scope.

    package main
    
    import (
        "os"
        "time"
    
        "github.com/ebitengine/oto/v3"
        "github.com/hajimehoshi/go-mp3"
    )
    
    func main() {
        // Open the file for reading. Do NOT close before you finish playing!
        file, err := os.Open("./my-file.mp3")
        if err != nil {
            panic("opening my-file.mp3 failed: " + err.Error())
        }
        // Ensure file is closed only after playback is complete
        defer file.Close()
    
        decodedMp3, err := mp3.NewDecoder(file)
        if err != nil {
            panic("mp3.NewDecoder failed: " + err.Error())
        }
    
        // ... (Context setup and player creation as shown in memory example) ...
    }
  4. Prerequisites for Linux, FreeBSD, and OpenBSD

    main

    Oto uses PulseAudio on Linux and BSD systems via the github.com/jfreymuth/pulse package.

    • PulseAudio: If the server is not discovered automatically, set the PULSE_SERVER environment variable.
    • ALSA Fallback: If PulseAudio is unreachable, Oto falls back to ALSA. This requires libasound.so.2 to be present at runtime, but no development headers are needed at build time.
    • FreeBSD Cross-compiling: When building with CGO_ENABLED=0, you must use the following flag: -gcflags="github.com/ebitengine/purego/internal/fakecgo=-std".
  5. Prerequisites for iOS

    main

    To build for iOS, you must add the following frameworks to the 'Linked Frameworks and Libraries' section of your Xcode project:

    • AVFoundation.framework
    • AudioToolbox.framework

    If you do not have clang installed on macOS, you can install it via the terminal or by running xcode-select --install.

  6. How Oto works: Contexts and Players

    main

    Oto follows a hierarchical model for audio playback:

    1. Context: The main object that interacts with audio drivers. You must create exactly one Context for your application. It manages the global audio state (sample rate, channels, etc.).
    2. Player: Created from a Context using NewPlayer(r io.Reader). A single context can manage multiple players simultaneously. Each player consumes data from an io.Reader and queues it into an underlying buffer for playback.

    Important Constraints:

    • Single Context: Creating multiple contexts is NOT supported and will return an error.
    • Fixed Format: A context has a single sample rate and channel count. You cannot play multiple audio sources with different sample rates at the same time.
    • Reader Ownership: You cannot share the same io.Reader across multiple players.
  7. Play sounds from memory

    main

    For small audio files, you can read the entire file into memory and pass a reader (like bytes.NewReader) to the player. This is fast but uses more RAM for large files.

    Example using go-mp3 to play an MP3 from memory:

    package main
    
    import (
        "bytes"
        "time"
        "os"
    
        "github.com/ebitengine/oto/v3"
        "github.com/hajimehoshi/go-mp3"
    )
    
    func main() {
        fileBytes, err := os.ReadFile("./my-file.mp3")
        if err != nil {
            panic("reading my-file.mp3 failed: " + err.Error())
        }
    
        fileBytesReader := bytes.NewReader(fileBytes)
    
        decodedMp3, err := mp3.NewDecoder(fileBytesReader)
        if err != nil {
            panic("mp3.NewDecoder failed: " + err.Error())
        }
    
        op := &oto.NewContextOptions{}
        op.SampleRate = 44100
        op.ChannelCount = 2
        op.Format = oto.FormatSignedInt16LE
    
        otoCtx, readyChan, err := oto.NewContext(op)
        if err != nil {
            panic("oto.NewContext failed: " + err.Error())
        }
        <-readyChan
    
        player := otoCtx.NewPlayer(decodedMp3)
        player.Play()
    
        for player.IsPlaying() {
            time.Sleep(time.Millisecond)
        }
    }
  8. Configure an Oto Context

    main

    To initialize an Oto context, use oto.NewContext with an oto.NewContextOptions struct. Common configuration fields include:

    • SampleRate: Typically 44100 or 48000. Using other values may cause distortion.
    • ChannelCount: 1 for mono or 2 for stereo.
    • Format: The audio data format (e.g., oto.FormatSignedInt16LE).

    Note that the context configuration is immutable once the context is created.

    op := &oto.NewContextOptions{}
    op.SampleRate = 44100
    op.ChannelCount = 2
    op.Format = oto.FormatSignedInt16LE
    
    otoCtx, readyChan, err := oto.NewContext(op)
  9. Adjust Player buffer size

    main

    Players have an internal audio data buffer. You can check the current amount of buffered data using Player.BufferedSize().

    To change the size of this internal buffer, type-assert the player to the oto.BufferSizeSetter interface and call SetBufferSize(newBufferSize).

    myPlayer.(oto.BufferSizeSetter).SetBufferSize(newBufferSize)
  10. Configure NewContextOptions

    main

    When calling NewContext, use the NewContextOptions struct to define the audio environment.

    FieldTypeDescription
    SampleRateintSamples per second (e.g., 44100 or 48000). All sources in this context must match this rate.
    ChannelCountintNumber of channels. 1 for mono, 2 for stereo. No other values are supported.
    FormatFormatThe bit depth and type of the audio data (see Format constants).
    BufferSizetime.DurationAdjusts the underlying device buffer. Smaller values reduce latency but may cause glitch noises; larger values increase latency but reduce noise. If 0, the driver default is used.
    ApplicationNamestringThe name of your application (used by systems like PulseAudio for volume control).
  11. Monitor buffer size and errors in Player

    main
    Use BufferedSize to check the number of bytes currently in the buffer that have not yet been sent to the audio hardware. Use Err to check if the player has encountered any audio errors.