2023-04-09 04:26:08 +00:00
|
|
|
//go:build !js && !wasm
|
|
|
|
// +build !js,!wasm
|
2020-09-02 03:54:58 +00:00
|
|
|
|
|
|
|
package sound
|
|
|
|
|
|
|
|
import (
|
2023-12-02 22:15:41 +00:00
|
|
|
"errors"
|
|
|
|
"os"
|
2020-09-02 03:54:58 +00:00
|
|
|
"path/filepath"
|
2023-12-02 22:15:41 +00:00
|
|
|
"strings"
|
2020-09-02 03:54:58 +00:00
|
|
|
"sync"
|
|
|
|
|
2022-09-24 22:17:25 +00:00
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
2020-09-02 03:54:58 +00:00
|
|
|
"git.kirsle.net/go/audio"
|
|
|
|
"git.kirsle.net/go/audio/sdl"
|
|
|
|
"github.com/veandco/go-sdl2/mix"
|
|
|
|
)
|
|
|
|
|
|
|
|
// SDL engine globals.
|
|
|
|
var (
|
|
|
|
engine *sdl.Engine
|
|
|
|
|
|
|
|
// Cache of loaded music and sound effects.
|
|
|
|
music = map[string]*sdl.Track{}
|
|
|
|
sounds = map[string]*sdl.Track{}
|
|
|
|
mu sync.RWMutex
|
2023-12-02 22:15:41 +00:00
|
|
|
|
|
|
|
// Supported file extensions, in preference order.
|
|
|
|
SupportedSoundExtensions = []string{
|
|
|
|
".wav",
|
|
|
|
".ogg",
|
|
|
|
".mp3",
|
|
|
|
}
|
2020-09-02 03:54:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Initialize SDL2 Audio at startup.
|
|
|
|
func init() {
|
|
|
|
eng, err := sdl.New(mix.INIT_MP3 | mix.INIT_OGG)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("sound.init(): error initializing SDL2 audio: %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = eng.Setup()
|
|
|
|
if err != nil {
|
|
|
|
log.Error("sound.init(): error setting up SDL2 audio: %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
engine = eng
|
|
|
|
Enabled = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// LoadMusic loads filename from the MusicRoot into the global music cache.
|
|
|
|
// If the music is already loaded, does nothing.
|
|
|
|
func LoadMusic(filename string) audio.Playable {
|
|
|
|
if engine == nil || !Enabled {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if the music is already loaded.
|
|
|
|
mu.RLock()
|
|
|
|
mus, ok := music[filename]
|
|
|
|
mu.RUnlock()
|
|
|
|
if ok {
|
|
|
|
return mus
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load the music in.
|
|
|
|
track, err := engine.LoadMusic(filepath.Join(MusicRoot, filename))
|
|
|
|
if err != nil {
|
|
|
|
log.Error("sound.LoadMusic: failed to load file %s: %s", filename, err)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
mu.Lock()
|
|
|
|
music[filename] = &track
|
|
|
|
mu.Unlock()
|
|
|
|
|
|
|
|
return &track
|
|
|
|
}
|
|
|
|
|
|
|
|
// LoadSound loads filename from the SoundRoot into the global SFX cache.
|
|
|
|
// If the sound is already loaded, does nothing.
|
|
|
|
func LoadSound(filename string) audio.Playable {
|
|
|
|
if engine == nil || !Enabled {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if the music is already loaded.
|
|
|
|
mu.RLock()
|
|
|
|
sfx, ok := sounds[filename]
|
|
|
|
mu.RUnlock()
|
|
|
|
if ok {
|
|
|
|
return sfx
|
|
|
|
}
|
|
|
|
|
2023-12-02 22:15:41 +00:00
|
|
|
// Resolve the filepath on disk to this sound.
|
|
|
|
fullpath, err := ResolveFilename(filename)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Loading sound: %s: %s", filename, err)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-09-02 03:54:58 +00:00
|
|
|
// Load the sound in.
|
|
|
|
log.Info("Loading sound: %s", filename)
|
2023-12-02 22:15:41 +00:00
|
|
|
track, err := engine.LoadSound(fullpath)
|
2020-09-02 03:54:58 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Error("sound.LoadSound: failed to load file %s: %s", filename, err)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
mu.Lock()
|
|
|
|
sounds[filename] = &track
|
|
|
|
mu.Unlock()
|
|
|
|
|
|
|
|
return &track
|
|
|
|
}
|
|
|
|
|
2023-04-09 04:26:08 +00:00
|
|
|
// PlaySound plays the named sound. It will de-duplicate if the same sound is already playing.
|
2020-09-02 03:54:58 +00:00
|
|
|
func PlaySound(filename string) {
|
|
|
|
log.Debug("Play sound: %s", filename)
|
|
|
|
sound := LoadSound(filename)
|
2023-04-09 04:26:08 +00:00
|
|
|
if sound != nil && !sound.Playing() {
|
2020-09-02 03:54:58 +00:00
|
|
|
sound.Play(1)
|
|
|
|
}
|
|
|
|
}
|
2023-12-02 22:15:41 +00:00
|
|
|
|
|
|
|
// ResolveFilename resolves the filename to a sound file on disk.
|
|
|
|
//
|
|
|
|
// Ogg has been found more reliable than MP3 for cross-platform distribution. A doodad might
|
|
|
|
// request a "sound.mp3" but the filename on disk is actually "sound.ogg" and this function
|
|
|
|
// will return the latter, if "sound.mp3" does not exist.
|
|
|
|
//
|
|
|
|
// If the exact filename does exist, it is returned; otherwise a preference order of
|
|
|
|
// WAV > OGG > MP3 will be tried and returned if those versions exist.
|
|
|
|
//
|
|
|
|
// Returns the full path on disk (e.g. "rtp/sfx/sound.ogg")
|
|
|
|
func ResolveFilename(filename string) (string, error) {
|
|
|
|
// Does the exact file exist?
|
|
|
|
if _, err := os.Stat(filepath.Join(SoundRoot, filename)); !os.IsNotExist(err) {
|
|
|
|
return filepath.Join(SoundRoot, filename), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the filename ends with a supported extension, trim it to the basename.
|
|
|
|
var basename = filename
|
|
|
|
for _, ext := range SupportedSoundExtensions {
|
|
|
|
if filepath.Ext(filename) == ext {
|
|
|
|
basename = strings.TrimSuffix(filename, ext)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try the basename + suffixes.
|
|
|
|
for _, ext := range SupportedSoundExtensions {
|
|
|
|
check := filepath.Join(SoundRoot, basename+ext)
|
|
|
|
if _, err := os.Stat(check); !os.IsNotExist(err) {
|
|
|
|
log.Info("Sound(%s): resolved to nearest match %s", filename, check)
|
|
|
|
return check, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No luck.
|
|
|
|
return "", errors.New("no suitable sound file found")
|
|
|
|
}
|