Initial code for basic SDL2 audio engine
This commit is contained in:
commit
ae3b0695ba
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.mp3
|
||||
*.ogg
|
21
LICENSE.md
Normal file
21
LICENSE.md
Normal 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
50
README.md
Normal 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
28
examples/play/README.md
Normal 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
118
examples/play/main.go
Normal 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
44
interface.go
Normal 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
87
null/null.go
Normal 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
13
null_test.go
Normal 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
127
sdl/music.go
Normal 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
78
sdl/sdl.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user