@@ -0,0 +1,2 @@ | |||
*.mp3 | |||
*.ogg |
@@ -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. |
@@ -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. |
@@ -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. | |||
``` |
@@ -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 | |||
} |
@@ -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 | |||
} |
@@ -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 | |||
} |
@@ -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() | |||
} |
@@ -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 | |||
} |
@@ -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 | |||
} |