From ae3b0695ba6f76a017961e2c437c87b7becb2d1e Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Tue, 28 Apr 2020 22:54:51 -0700 Subject: [PATCH] Initial code for basic SDL2 audio engine --- .gitignore | 2 + LICENSE.md | 21 +++++++ README.md | 50 ++++++++++++++++ examples/play/README.md | 28 +++++++++ examples/play/main.go | 118 +++++++++++++++++++++++++++++++++++++ interface.go | 44 ++++++++++++++ null/null.go | 87 +++++++++++++++++++++++++++ null_test.go | 13 ++++ sdl/music.go | 127 ++++++++++++++++++++++++++++++++++++++++ sdl/sdl.go | 78 ++++++++++++++++++++++++ 10 files changed, 568 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 examples/play/README.md create mode 100644 examples/play/main.go create mode 100644 interface.go create mode 100644 null/null.go create mode 100644 null_test.go create mode 100644 sdl/music.go create mode 100644 sdl/sdl.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..19acb48 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.mp3 +*.ogg diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..acc15af --- /dev/null +++ b/LICENSE.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4a6090 --- /dev/null +++ b/README.md @@ -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. diff --git a/examples/play/README.md b/examples/play/README.md new file mode 100644 index 0000000..6e53bbb --- /dev/null +++ b/examples/play/README.md @@ -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 + Loop the audio file, default 0 will only play it once. -1 for music will + play forever. +``` diff --git a/examples/play/main.go b/examples/play/main.go new file mode 100644 index 0000000..2dd8367 --- /dev/null +++ b/examples/play/main.go @@ -0,0 +1,118 @@ +// Example program using the SDL2 Mix engine. +// +// Plays a music or sound file from disk. +// Usage: `play ` +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 ") + 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 +} diff --git a/interface.go b/interface.go new file mode 100644 index 0000000..39fa5a1 --- /dev/null +++ b/interface.go @@ -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 +} diff --git a/null/null.go b/null/null.go new file mode 100644 index 0000000..ea61d1f --- /dev/null +++ b/null/null.go @@ -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 +} diff --git a/null_test.go b/null_test.go new file mode 100644 index 0000000..bc28490 --- /dev/null +++ b/null_test.go @@ -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() +} diff --git a/sdl/music.go b/sdl/music.go new file mode 100644 index 0000000..285d114 --- /dev/null +++ b/sdl/music.go @@ -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 +} diff --git a/sdl/sdl.go b/sdl/sdl.go new file mode 100644 index 0000000..f27fe76 --- /dev/null +++ b/sdl/sdl.go @@ -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 +}