Initial code for basic SDL2 audio engine

This commit is contained in:
Noah 2020-04-28 22:54:51 -07:00
commit ae3b0695ba
10 changed files with 568 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.mp3
*.ogg

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
# The MIT License (MIT)
Copyright (c) 2020 Noah Petherbridge
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

50
README.md Normal file
View File

@ -0,0 +1,50 @@
# audio: Simple Audio Engine for Go
Package `audio` is a simple audio engine for Go that can play some music and
sound files. It currently supports an SDL2 (Mixer) driver suitable for
use on desktop systems like Linux, Mac OS and Windows, with support to load
and play music files (.ogg and .mp3 format, depending on your system libraries)
and sound effects (.wav).
## Example
See the `examples/play/main.go` for a simple command-line media player sample
that uses the SDL2 engine.
```go
package main
import (
"time"
"git.kirsle.net/go/audio/sdl"
)
func main() {
sfx, err := sdl.New(mix.INIT_MP3 | mix.INIT_OGG)
if err != nil {
panic(err)
}
// Call once at program startup.
sfx.Setup()
defer sfx.Teardown()
// Load a file from disk.
music, err := sfx.LoadMusic("filename.mp3")
if err != nil {
panic(err)
}
// Play it.
music.Play(0)
// Wait until done.
for sfx.Playing() {
time.Sleep(1 * time.Second)
}
}
```
## License
MIT.

28
examples/play/README.md Normal file
View File

@ -0,0 +1,28 @@
# Example: `play`
This example uses the SDL2 Mixer engine to implement a _simple_ command line
program that plays music and sound files.
## Usage
```
play [options] path/to/file.ogg
```
Supports file types `.ogg`, `.mp3` and `.wav`.
Default behavior calls LoadSound() or LoadMusic() using the filename given.
Use the `-binary` option to go through LoadSoundBin() or LoadMusicBin() to
test initializing it by bytes array instead.
### Options
```
-binary
Opens the file first as a bytes array, and feeds it to the audio engine
as binary data instead of by passing a filename on disk.
-loop <int>
Loop the audio file, default 0 will only play it once. -1 for music will
play forever.
```

118
examples/play/main.go Normal file
View File

@ -0,0 +1,118 @@
// Example program using the SDL2 Mix engine.
//
// Plays a music or sound file from disk.
// Usage: `play <filepath>`
package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
"git.kirsle.net/go/audio/sdl"
"github.com/veandco/go-sdl2/mix"
)
// CLI flags.
var (
flagBinary bool
flagLoop int
)
func init() {
flag.BoolVar(&flagBinary, "binary", false,
"Feed the file as bytes instead of by filename on disk.")
flag.IntVar(&flagLoop, "loop", 0, "Number of times to loop the audio.")
}
func main() {
flag.Parse()
if len(os.Args) < 2 {
fmt.Println("Usage: play <path/to/file.mp3>")
fmt.Println("Supports .ogg, .mp3, .wav")
os.Exit(1)
}
sfx, err := sdl.New(mix.INIT_MP3 | mix.INIT_OGG)
if err != nil {
panic(err)
}
sfx.Setup()
var (
filename = os.Args[len(os.Args)-1]
sound sdl.Track
)
if flagBinary {
fmt.Printf("Opening '%s' and feeding to engine as binary\n", filename)
sound = loadBinary(sfx, filename)
} else {
fmt.Printf("Playing '%s' from filesystem\n", filename)
sound = loadFilename(sfx, filename)
}
fmt.Printf("Begin playback")
sound.Play(flagLoop)
for sfx.Playing() {
time.Sleep(1 * time.Second)
fmt.Print(".")
}
fmt.Println("Done.")
}
func loadFilename(sfx *sdl.Engine, filename string) sdl.Track {
var (
sound sdl.Track
err error
)
switch filepath.Ext(filename) {
case ".ogg", ".mp3":
sound, err = sfx.LoadMusic(filename)
if err != nil {
panic(err)
}
case ".wav":
sound, err = sfx.LoadSound(filename)
if err != nil {
panic(err)
}
default:
panic("Unsupported file type")
}
return sound
}
// Loads the audio file by sending the byte stream into the audio engine
// instead of having the audio engine open it from filesystem itself.
func loadBinary(sfx *sdl.Engine, filename string) sdl.Track {
bin, err := ioutil.ReadFile(filename)
if err != nil {
panic(err)
}
var sound sdl.Track
switch filepath.Ext(filename) {
case ".ogg", ".mp3":
sound, err = sfx.LoadMusicBin(bin)
if err != nil {
panic(err)
}
case ".wav":
sound, err = sfx.LoadSoundBin(bin)
if err != nil {
panic(err)
}
default:
panic("Unsupported file type: " + filepath.Ext(filename))
}
return sound
}

44
interface.go Normal file
View File

@ -0,0 +1,44 @@
package audio
// Engine is a music and sound effects driver.
type Engine interface {
// Setup runs initialization tasks for the audio engine.
Setup() error
// Teardown runs closing tasks for the audio engine to shut down gracefully.
Teardown() error
// Playing returns a bool if something is actively playing.
// PlayingMusic and PlayingSound check specifically if music or sound
// effects are currently playing.
Playing() bool
PlayingMusic() bool
PlayingSound() bool
// StopAll stops all music and sound effects.
// StopMusic and StopSounds to selectively stop either the music or all
// sound effects, respectively.
StopAll()
StopMusic()
StopSounds()
// LoadMusic opens a music file from disk and loads it into memory.
// LoadMusicBin to load file by bytes in memory instead.
LoadMusic(filename string) (Playable, error)
LoadMusicBin(data []byte) (Playable, error)
// LoadSound opens a sound effect file.
// LoadSoundBin to load file by bytes in memory instead.
LoadSound(filename string) (Playable, error)
LoadSoundBin(data []byte) (Playable, error)
}
// Playable is a music or sound effect object that can be played and managed.
type Playable interface {
Play(loops int) error
Pause() error
Stop() error
// Destroy deallocates and frees the memory.
Destroy() error
}

87
null/null.go Normal file
View File

@ -0,0 +1,87 @@
// Package null implements a dummy audio driver that doesn't play any audio.
package null
// Engine is a null audio engine.
type Engine struct{}
// Playable is a null music or sound effect.
type Playable struct{}
// New creates a null engine.
func New() *Engine {
return &Engine{}
}
// Setup the null engine (do nothing).
func (e *Engine) Setup() error {
return nil
}
// Teardown the null engine (do nothing).
func (e *Engine) Teardown() error {
return nil
}
// Playing returns false.
func (e *Engine) Playing() bool {
return false
}
// PlayingMusic returns false.
func (e *Engine) PlayingMusic() bool {
return false
}
// PlayingSound returns false.
func (e *Engine) PlayingSound() bool {
return false
}
// StopAll does nothing.
func (e *Engine) StopAll() {}
// StopMusic does nothing.
func (e *Engine) StopMusic() {}
// StopSounds does nothing.
func (e *Engine) StopSounds() {}
// LoadMusic loads nothing.
func (e *Engine) LoadMusic(filename string) (Playable, error) {
return Playable{}, nil
}
// LoadMusicBin loads nothing.
func (e *Engine) LoadMusicBin(data []byte) (Playable, error) {
return Playable{}, nil
}
// LoadSound loads nothing.
func (e *Engine) LoadSound(filename string) (Playable, error) {
return Playable{}, nil
}
// LoadSoundBin loads nothing.
func (e *Engine) LoadSoundBin(data []byte) (Playable, error) {
return Playable{}, nil
}
// Play nothing.
func (p Playable) Play(loops int) error {
return nil
}
// Pause nothing
func (p Playable) Pause() error {
return nil
}
// Stop nothing.
func (p Playable) Stop() error {
return nil
}
// Destroy nothing.
func (p Playable) Destroy() error {
return nil
}

13
null_test.go Normal file
View File

@ -0,0 +1,13 @@
package audio_test
import (
"testing"
"git.kirsle.net/go/audio/null"
)
func TestNullEngine(t *testing.T) {
null := null.New()
null.Setup()
null.Teardown()
}

127
sdl/music.go Normal file
View File

@ -0,0 +1,127 @@
package sdl
import (
"github.com/veandco/go-sdl2/mix"
"github.com/veandco/go-sdl2/sdl"
)
// Track is a music or sound effect file.
type Track struct {
isMusic bool // false = is sound effect
// If isMusic
mus *mix.Music
// Sound effect
wav *mix.Chunk
channel int
}
// LoadMusic loads a music file from disk.
func (e *Engine) LoadMusic(filename string) (Track, error) {
mus, err := mix.LoadMUS(filename)
return Track{
isMusic: true,
mus: mus,
}, err
}
// LoadMusicBin loads a music file from bytes data.
func (e *Engine) LoadMusicBin(data []byte) (Track, error) {
// Create an SDL RWOps from the binary.
rw, err := sdl.RWFromMem(data)
if err != nil {
return Track{}, err
}
mus, err := mix.LoadMUSRW(rw, 0)
return Track{
isMusic: true,
mus: mus,
}, err
}
// LoadSound loads a wave file from disk.
func (e *Engine) LoadSound(filename string) (Track, error) {
wav, err := mix.LoadWAV(filename)
return Track{
isMusic: false,
wav: wav,
channel: -1,
}, err
}
// LoadSoundBin loads a wave file from bytes data.
func (e *Engine) LoadSoundBin(data []byte) (Track, error) {
// Create an SDL RWOps from the binary.
rw, err := sdl.RWFromMem(data)
if err != nil {
return Track{}, err
}
wav, err := mix.LoadWAVRW(rw, false)
return Track{
isMusic: false,
wav: wav,
channel: -1,
}, err
}
// Play the track.
func (t *Track) Play(loops int) error {
if t.isMusic {
return t.mus.Play(loops)
}
// Normalize the `loops` value for sound effects to work around
// a quirk in the SDL mixer between Music and Sound behaviors:
//
// For music:
// loops=0 plays it one time
// loops=1 plays it one time
// loops=2 plays it twice
// For sounds:
// loops=0 plays it one time
// loops=1 plays it twice!
// loops=2 plays it three times!
//
// For both, a loops of -1 plays it on an infinite loop. So to make
// the API consistent on our end, subtract 1 from a Sound loop only
// when the given value is >= 1 itself.
if loops > 0 {
loops--
}
channel, err := t.wav.Play(-1, loops)
t.channel = channel
return err
}
// Pause the track.
func (t Track) Pause() error {
if t.isMusic {
mix.PauseMusic()
return nil
}
mix.Pause(t.channel)
return nil
}
// Stop the track.
func (t Track) Stop() error {
if t.isMusic {
mix.HaltMusic()
return nil
}
mix.HaltChannel(t.channel)
return nil
}
// Destroy the track.
func (t Track) Destroy() error {
if t.isMusic {
t.mus.Free()
return nil
}
t.wav.Free()
return nil
}

78
sdl/sdl.go Normal file
View File

@ -0,0 +1,78 @@
// Package sdl implements an audio engine using libSDL2.
package sdl
import "github.com/veandco/go-sdl2/mix"
// Engine is the SDL2 audio engine.
type Engine struct {
initFlags int
}
// New initializes an SDL2 Mixer for the audio engine.
//
// Pass the SDL2 Mixer flags for its initialization. The flags are an OR'd
// value made up of:
// mix.INIT_MP3
// mix.INIT_OGG
// mix.INIT_FLAC
// mix.INIT_MOD
func New(flags int) (*Engine, error) {
return &Engine{
initFlags: flags,
}, nil
}
// Setup initializes SDL2 Mixer.
func (e *Engine) Setup() error {
// Initialize SDL2 mixer.
if err := mix.Init(e.initFlags); err != nil {
return err
}
// Open the audio mixer.
// the '2' is stereo (two channels), '1' would be mono.
// 4096 is the chunk size.
// https://www.libsdl.org/projects/SDL_mixer/docs/SDL_mixer_11.html
return mix.OpenAudio(mix.DEFAULT_FREQUENCY, mix.DEFAULT_FORMAT, 2, 4096)
}
// Playing returns if either music or sounds are currently playing.
func (e *Engine) Playing() bool {
if mix.PlayingMusic() {
return true
}
return mix.Playing(-1) > 0
}
// PlayingMusic returns if music is currently playing.
func (e *Engine) PlayingMusic() bool {
return mix.PlayingMusic()
}
// PlayingSound returns if sounds are playing.
func (e *Engine) PlayingSound() bool {
return mix.Playing(-1) > 0
}
// StopAll stops all music and sounds.
func (e *Engine) StopAll() {
e.StopMusic()
e.StopSounds()
}
// StopMusic stops all music.
func (e *Engine) StopMusic() {
mix.HaltMusic()
}
// StopSounds stops all sounds.
func (e *Engine) StopSounds() {
mix.HaltChannel(-1)
}
// Teardown closes the SDL mixer.
func (e *Engine) Teardown() error {
mix.CloseAudio()
mix.Quit()
return nil
}