mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-09-25 20:55:55 -04:00
Sound engine and sound environments (#652)
* Working sound engine and sound environments * Clean up sounds.txt loader * Make global volume settings apply properly Als shuffle some stuff around * Reset sound engine on game unload
This commit is contained in:
parent
78ecc3557e
commit
29ea71489d
@ -36,11 +36,11 @@ type SoundEnvironRecord struct {
|
|||||||
|
|
||||||
// SoundEnvirons contains the SoundEnviron records
|
// SoundEnvirons contains the SoundEnviron records
|
||||||
//nolint:gochecknoglobals // Currently global by design, only written once
|
//nolint:gochecknoglobals // Currently global by design, only written once
|
||||||
var SoundEnvirons map[string]*SoundEnvironRecord
|
var SoundEnvirons map[int]*SoundEnvironRecord
|
||||||
|
|
||||||
// LoadSoundEnvirons loads SoundEnvirons from the supplied file
|
// LoadSoundEnvirons loads SoundEnvirons from the supplied file
|
||||||
func LoadSoundEnvirons(file []byte) {
|
func LoadSoundEnvirons(file []byte) {
|
||||||
SoundEnvirons = make(map[string]*SoundEnvironRecord)
|
SoundEnvirons = make(map[int]*SoundEnvironRecord)
|
||||||
|
|
||||||
d := d2common.LoadDataDictionary(file)
|
d := d2common.LoadDataDictionary(file)
|
||||||
for d.Next() {
|
for d.Next() {
|
||||||
@ -70,7 +70,7 @@ func LoadSoundEnvirons(file []byte) {
|
|||||||
EAXRoomRoll: d.Number("EAX Room Roll"),
|
EAXRoomRoll: d.Number("EAX Room Roll"),
|
||||||
EAXAirAbsorb: d.Number("EAX Air Absorb"),
|
EAXAirAbsorb: d.Number("EAX Air Absorb"),
|
||||||
}
|
}
|
||||||
SoundEnvirons[record.Handle] = record
|
SoundEnvirons[record.Index] = record
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.Err != nil {
|
if d.Err != nil {
|
||||||
|
@ -2,7 +2,6 @@ package d2datadict
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||||
)
|
)
|
||||||
@ -12,69 +11,30 @@ type SoundEntry struct {
|
|||||||
Handle string
|
Handle string
|
||||||
Index int
|
Index int
|
||||||
FileName string
|
FileName string
|
||||||
Volume byte
|
Volume int
|
||||||
GroupSize uint8
|
GroupSize int
|
||||||
Loop bool
|
Loop bool
|
||||||
FadeIn uint8
|
FadeIn int
|
||||||
FadeOut uint8
|
FadeOut int
|
||||||
DeferInst uint8
|
DeferInst bool
|
||||||
StopInst uint8
|
StopInst bool
|
||||||
Duration uint8
|
Duration int
|
||||||
Compound int8
|
Compound int
|
||||||
Reverb bool
|
Reverb int
|
||||||
Falloff uint8
|
Falloff int
|
||||||
Cache uint8
|
Cache bool
|
||||||
AsyncOnly bool
|
AsyncOnly bool
|
||||||
Priority uint8
|
Priority int
|
||||||
Stream uint8
|
Stream bool
|
||||||
Stereo uint8
|
Stereo bool
|
||||||
Tracking uint8
|
Tracking bool
|
||||||
Solo uint8
|
Solo bool
|
||||||
MusicVol uint8
|
MusicVol bool
|
||||||
Block1 int
|
Block1 int
|
||||||
Block2 int
|
Block2 int
|
||||||
Block3 int
|
Block3 int
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateSoundEntry creates a sound entry based on a sound row on sounds.txt
|
|
||||||
func createSoundEntry(soundLine string) SoundEntry {
|
|
||||||
props := strings.Split(soundLine, "\t")
|
|
||||||
i := -1
|
|
||||||
inc := func() int {
|
|
||||||
i++
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
result := SoundEntry{
|
|
||||||
Handle: props[inc()],
|
|
||||||
Index: d2common.StringToInt(props[inc()]),
|
|
||||||
FileName: props[inc()],
|
|
||||||
Volume: d2common.StringToUint8(props[inc()]),
|
|
||||||
GroupSize: d2common.StringToUint8(props[inc()]),
|
|
||||||
Loop: d2common.StringToUint8(props[inc()]) == 1,
|
|
||||||
FadeIn: d2common.StringToUint8(props[inc()]),
|
|
||||||
FadeOut: d2common.StringToUint8(props[inc()]),
|
|
||||||
DeferInst: d2common.StringToUint8(props[inc()]),
|
|
||||||
StopInst: d2common.StringToUint8(props[inc()]),
|
|
||||||
Duration: d2common.StringToUint8(props[inc()]),
|
|
||||||
Compound: d2common.StringToInt8(props[inc()]),
|
|
||||||
Reverb: d2common.StringToUint8(props[inc()]) == 1,
|
|
||||||
Falloff: d2common.StringToUint8(props[inc()]),
|
|
||||||
Cache: d2common.StringToUint8(props[inc()]),
|
|
||||||
AsyncOnly: d2common.StringToUint8(props[inc()]) == 1,
|
|
||||||
Priority: d2common.StringToUint8(props[inc()]),
|
|
||||||
Stream: d2common.StringToUint8(props[inc()]),
|
|
||||||
Stereo: d2common.StringToUint8(props[inc()]),
|
|
||||||
Tracking: d2common.StringToUint8(props[inc()]),
|
|
||||||
Solo: d2common.StringToUint8(props[inc()]),
|
|
||||||
MusicVol: d2common.StringToUint8(props[inc()]),
|
|
||||||
Block1: d2common.StringToInt(props[inc()]),
|
|
||||||
Block2: d2common.StringToInt(props[inc()]),
|
|
||||||
Block3: d2common.StringToInt(props[inc()]),
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sounds stores all of the SoundEntries
|
// Sounds stores all of the SoundEntries
|
||||||
//nolint:gochecknoglobals // Currently global by design, only written once
|
//nolint:gochecknoglobals // Currently global by design, only written once
|
||||||
var Sounds map[string]SoundEntry
|
var Sounds map[string]SoundEntry
|
||||||
@ -82,31 +42,53 @@ var Sounds map[string]SoundEntry
|
|||||||
// LoadSounds loads SoundEntries from sounds.txt
|
// LoadSounds loads SoundEntries from sounds.txt
|
||||||
func LoadSounds(file []byte) {
|
func LoadSounds(file []byte) {
|
||||||
Sounds = make(map[string]SoundEntry)
|
Sounds = make(map[string]SoundEntry)
|
||||||
soundData := strings.Split(string(file), "\r\n")[1:]
|
|
||||||
|
|
||||||
for _, line := range soundData {
|
d := d2common.LoadDataDictionary(file)
|
||||||
if line == "" {
|
for d.Next() {
|
||||||
continue
|
entry := SoundEntry{
|
||||||
|
Handle: d.String("Sound"),
|
||||||
|
Index: d.Number("Index"),
|
||||||
|
FileName: d.String("FileName"),
|
||||||
|
Volume: d.Number("Volume"),
|
||||||
|
GroupSize: d.Number("Group Size"),
|
||||||
|
Loop: d.Bool("Loop"),
|
||||||
|
FadeIn: d.Number("Fade In"),
|
||||||
|
FadeOut: d.Number("Fade Out"),
|
||||||
|
DeferInst: d.Bool("Defer Inst"),
|
||||||
|
StopInst: d.Bool("Stop Inst"),
|
||||||
|
Duration: d.Number("Duration"),
|
||||||
|
Compound: d.Number("Compound"),
|
||||||
|
Reverb: d.Number("Reverb"),
|
||||||
|
Falloff: d.Number("Falloff"),
|
||||||
|
Cache: d.Bool("Cache"),
|
||||||
|
AsyncOnly: d.Bool("Async Only"),
|
||||||
|
Priority: d.Number("Priority"),
|
||||||
|
Stream: d.Bool("Stream"),
|
||||||
|
Stereo: d.Bool("Stereo"),
|
||||||
|
Tracking: d.Bool("Tracking"),
|
||||||
|
Solo: d.Bool("Solo"),
|
||||||
|
MusicVol: d.Bool("Music Vol"),
|
||||||
|
Block1: d.Number("Block 1"),
|
||||||
|
Block2: d.Number("Block 2"),
|
||||||
|
Block3: d.Number("Block 3"),
|
||||||
}
|
}
|
||||||
|
Sounds[entry.Handle] = entry
|
||||||
|
}
|
||||||
|
|
||||||
soundEntry := createSoundEntry(line)
|
if d.Err != nil {
|
||||||
soundEntry.FileName = "/data/global/sfx/" + strings.ReplaceAll(soundEntry.FileName, `\`, "/")
|
panic(d.Err)
|
||||||
Sounds[soundEntry.Handle] = soundEntry
|
}
|
||||||
|
|
||||||
//nolint:gocritic // Debug util code
|
|
||||||
/*
|
|
||||||
// Use the following code to write out the values
|
|
||||||
f, err := os.OpenFile(`C:\Users\lunat\Desktop\D2\sounds.txt`,
|
|
||||||
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
if _, err := f.WriteString("\n[" + soundEntry.Handle + "] " + soundEntry.FileName); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
} //nolint:wsl // Debug util code
|
|
||||||
|
|
||||||
log.Printf("Loaded %d sound definitions", len(Sounds))
|
log.Printf("Loaded %d sound definitions", len(Sounds))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelectSoundByIndex selects a sound by its ID
|
||||||
|
func SelectSoundByIndex(index int) *SoundEntry {
|
||||||
|
for _, el := range Sounds {
|
||||||
|
if el.Index == index {
|
||||||
|
return &el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -4,6 +4,6 @@ package d2interface
|
|||||||
// by the asset manager, and set the game engine's volume levels
|
// by the asset manager, and set the game engine's volume levels
|
||||||
type AudioProvider interface {
|
type AudioProvider interface {
|
||||||
PlayBGM(song string)
|
PlayBGM(song string)
|
||||||
LoadSoundEffect(sfx string) (SoundEffect, error)
|
LoadSound(sfx string, loop bool, bgm bool) (SoundEffect, error)
|
||||||
SetVolumes(bgmVolume, sfxVolume float64)
|
SetVolumes(bgmVolume, sfxVolume float64)
|
||||||
}
|
}
|
||||||
|
@ -4,4 +4,6 @@ package d2interface
|
|||||||
type SoundEffect interface {
|
type SoundEffect interface {
|
||||||
Play()
|
Play()
|
||||||
Stop()
|
Stop()
|
||||||
|
IsPlaying() bool
|
||||||
|
SetVolume(volume float64)
|
||||||
}
|
}
|
||||||
|
@ -290,12 +290,12 @@ const (
|
|||||||
SFXButtonClick = "cursor_button_click"
|
SFXButtonClick = "cursor_button_click"
|
||||||
SFXAmazonDeselect = "cursor_amazon_deselect"
|
SFXAmazonDeselect = "cursor_amazon_deselect"
|
||||||
SFXAmazonSelect = "cursor_amazon_select"
|
SFXAmazonSelect = "cursor_amazon_select"
|
||||||
SFXAssassinDeselect = "/data/global/sfx/Cursor/intro/assassin deselect.wav"
|
SFXAssassinDeselect = "Cursor/intro/assassin deselect.wav"
|
||||||
SFXAssassinSelect = "/data/global/sfx/Cursor/intro/assassin select.wav"
|
SFXAssassinSelect = "Cursor/intro/assassin select.wav"
|
||||||
SFXBarbarianDeselect = "cursor_barbarian_deselect"
|
SFXBarbarianDeselect = "cursor_barbarian_deselect"
|
||||||
SFXBarbarianSelect = "cursor_barbarian_select"
|
SFXBarbarianSelect = "cursor_barbarian_select"
|
||||||
SFXDruidDeselect = "/data/global/sfx/Cursor/intro/druid deselect.wav"
|
SFXDruidDeselect = "Cursor/intro/druid deselect.wav"
|
||||||
SFXDruidSelect = "/data/global/sfx/Cursor/intro/druid select.wav"
|
SFXDruidSelect = "Cursor/intro/druid select.wav"
|
||||||
SFXNecromancerDeselect = "cursor_necromancer_deselect"
|
SFXNecromancerDeselect = "cursor_necromancer_deselect"
|
||||||
SFXNecromancerSelect = "cursor_necromancer_select"
|
SFXNecromancerSelect = "cursor_necromancer_select"
|
||||||
SFXPaladinDeselect = "cursor_paladin_deselect"
|
SFXPaladinDeselect = "cursor_paladin_deselect"
|
||||||
|
@ -85,7 +85,7 @@ func (d *DataDictionary) List(field string) []string {
|
|||||||
func (d *DataDictionary) Bool(field string) bool {
|
func (d *DataDictionary) Bool(field string) bool {
|
||||||
n := d.Number(field)
|
n := d.Number(field)
|
||||||
if n > 1 {
|
if n > 1 {
|
||||||
log.Panic("Bool on non-bool field")
|
log.Panic("Bool on non-bool field ", field)
|
||||||
}
|
}
|
||||||
|
|
||||||
return n == 1
|
return n == 1
|
||||||
|
@ -97,8 +97,12 @@ func (eap *AudioProvider) PlayBGM(song string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadSoundEffect loads a sound affect so that it canb e played
|
// LoadSoundEffect loads a sound affect so that it canb e played
|
||||||
func (eap *AudioProvider) LoadSoundEffect(sfx string) (d2interface.SoundEffect, error) {
|
func (eap *AudioProvider) LoadSound(sfx string, loop bool, bgm bool) (d2interface.SoundEffect, error) {
|
||||||
result := CreateSoundEffect(sfx, eap.audioContext, eap.sfxVolume) // TODO: Split
|
volume := eap.sfxVolume
|
||||||
|
if bgm {
|
||||||
|
volume = eap.bgmVolume
|
||||||
|
}
|
||||||
|
result := CreateSoundEffect(sfx, eap.audioContext, volume, loop) // TODO: Split
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
@ -12,40 +12,53 @@ import (
|
|||||||
|
|
||||||
// SoundEffect represents an ebiten implementation of a sound effect
|
// SoundEffect represents an ebiten implementation of a sound effect
|
||||||
type SoundEffect struct {
|
type SoundEffect struct {
|
||||||
player *audio.Player
|
player *audio.Player
|
||||||
|
volumeScale float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateSoundEffect creates a new instance of ebiten's sound effect implementation.
|
// CreateSoundEffect creates a new instance of ebiten's sound effect implementation.
|
||||||
func CreateSoundEffect(sfx string, context *audio.Context, volume float64) *SoundEffect {
|
func CreateSoundEffect(sfx string, context *audio.Context, volume float64, loop bool) *SoundEffect {
|
||||||
result := &SoundEffect{}
|
result := &SoundEffect{}
|
||||||
|
|
||||||
var soundFile string
|
soundFile := "/data/global/sfx/"
|
||||||
|
|
||||||
if _, exists := d2datadict.Sounds[sfx]; exists {
|
if _, exists := d2datadict.Sounds[sfx]; exists {
|
||||||
soundEntry := d2datadict.Sounds[sfx]
|
soundEntry := d2datadict.Sounds[sfx]
|
||||||
soundFile = soundEntry.FileName
|
soundFile += soundEntry.FileName
|
||||||
} else {
|
} else {
|
||||||
soundFile = sfx
|
soundFile += sfx
|
||||||
}
|
}
|
||||||
|
|
||||||
audioData, err := d2asset.LoadFile(soundFile)
|
audioData, err := d2asset.LoadFileStream(soundFile)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
audioData, err = d2asset.LoadFileStream("/data/global/music/" + sfx)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
d, err := wav.Decode(context, audio.BytesReadSeekCloser(audioData))
|
d, err := wav.Decode(context, audioData)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
player, err := audio.NewPlayer(context, d)
|
var player *audio.Player
|
||||||
|
|
||||||
|
if loop {
|
||||||
|
s := audio.NewInfiniteLoop(d, d.Length())
|
||||||
|
player, err = audio.NewPlayer(context, s)
|
||||||
|
} else {
|
||||||
|
player, err = audio.NewPlayer(context, d)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.volumeScale = volume
|
||||||
player.SetVolume(volume)
|
player.SetVolume(volume)
|
||||||
|
|
||||||
result.player = player
|
result.player = player
|
||||||
@ -53,6 +66,14 @@ func CreateSoundEffect(sfx string, context *audio.Context, volume float64) *Soun
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *SoundEffect) SetVolume(volume float64) {
|
||||||
|
v.player.SetVolume(volume * v.volumeScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SoundEffect) IsPlaying() bool {
|
||||||
|
return v.player.IsPlaying()
|
||||||
|
}
|
||||||
|
|
||||||
// Play plays the sound effect
|
// Play plays the sound effect
|
||||||
func (v *SoundEffect) Play() {
|
func (v *SoundEffect) Play() {
|
||||||
err := v.player.Rewind()
|
err := v.player.Rewind()
|
||||||
|
195
d2core/d2audio/sound_engine.go
Normal file
195
d2core/d2audio/sound_engine.go
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
package d2audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
||||||
|
)
|
||||||
|
|
||||||
|
type envState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
envAttack = 0
|
||||||
|
envSustain = 1
|
||||||
|
envRelease = 2
|
||||||
|
envStopped = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
const volMax float64 = 255
|
||||||
|
const originalFPS float64 = 25
|
||||||
|
|
||||||
|
// A Sound that can be started and stopped
|
||||||
|
type Sound struct {
|
||||||
|
effect d2interface.SoundEffect
|
||||||
|
entry *d2datadict.SoundEntry
|
||||||
|
volume float64
|
||||||
|
vTarget float64
|
||||||
|
vRate float64
|
||||||
|
state envState
|
||||||
|
// panning float64 // lets forget about this for now
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sound) update(elapsed float64) {
|
||||||
|
// attack
|
||||||
|
if s.state == envAttack {
|
||||||
|
s.volume += s.vRate * elapsed
|
||||||
|
if s.volume > s.vTarget {
|
||||||
|
s.volume = s.vTarget
|
||||||
|
s.state = envSustain
|
||||||
|
}
|
||||||
|
|
||||||
|
s.effect.SetVolume(s.volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
// release
|
||||||
|
if s.state == envRelease {
|
||||||
|
s.volume -= s.vRate * elapsed
|
||||||
|
if s.volume < 0 {
|
||||||
|
s.effect.Stop()
|
||||||
|
s.volume = 0
|
||||||
|
s.state = envStopped
|
||||||
|
}
|
||||||
|
|
||||||
|
s.effect.SetVolume(s.volume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play the sound
|
||||||
|
func (s *Sound) Play() {
|
||||||
|
log.Println("starting sound", s.entry.Handle)
|
||||||
|
s.effect.Play()
|
||||||
|
|
||||||
|
if s.entry.FadeIn != 0 {
|
||||||
|
s.effect.SetVolume(0)
|
||||||
|
s.volume = 0
|
||||||
|
s.state = envAttack
|
||||||
|
s.vTarget = float64(s.entry.Volume) / volMax
|
||||||
|
s.vRate = (s.vTarget / (float64(s.entry.FadeIn) / originalFPS))
|
||||||
|
} else {
|
||||||
|
s.volume = float64(s.entry.Volume) / volMax
|
||||||
|
s.effect.SetVolume(s.volume)
|
||||||
|
s.state = envSustain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the sound, only required for looping sounds
|
||||||
|
func (s *Sound) Stop() {
|
||||||
|
if s.entry.FadeOut != 0 {
|
||||||
|
s.state = envRelease
|
||||||
|
s.vTarget = 0
|
||||||
|
s.vRate = (s.volume / (float64(s.entry.FadeOut) / originalFPS))
|
||||||
|
} else {
|
||||||
|
s.state = envStopped
|
||||||
|
s.volume = 0
|
||||||
|
s.effect.SetVolume(s.volume)
|
||||||
|
s.effect.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoundEngine provides functions for playing sounds
|
||||||
|
type SoundEngine struct {
|
||||||
|
provider d2interface.AudioProvider
|
||||||
|
timer float64
|
||||||
|
accTime float64
|
||||||
|
sounds map[*Sound]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSoundEngine creates a new sound engine
|
||||||
|
func NewSoundEngine(provider d2interface.AudioProvider, term d2interface.Terminal) *SoundEngine {
|
||||||
|
r := SoundEngine{
|
||||||
|
provider: provider,
|
||||||
|
sounds: map[*Sound]struct{}{},
|
||||||
|
timer: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = term.BindAction("playsoundid", "plays the sound for a given id", func(id int) {
|
||||||
|
r.PlaySoundID(id)
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = term.BindAction("playsound", "plays the sound for a given handle string", func(handle string) {
|
||||||
|
r.PlaySoundHandle(handle)
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = term.BindAction("activesounds", "list currently active sounds", func() {
|
||||||
|
for s := range r.sounds {
|
||||||
|
log.Println(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = term.BindAction("killsounds", "kill active sounds", func() {
|
||||||
|
for s := range r.sounds {
|
||||||
|
s.Stop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return &r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance updates sound engine state, triggering events and envelopes
|
||||||
|
func (s *SoundEngine) Advance(elapsed float64) {
|
||||||
|
s.timer -= elapsed
|
||||||
|
s.accTime += elapsed
|
||||||
|
|
||||||
|
if s.timer < 0 {
|
||||||
|
for sound := range s.sounds {
|
||||||
|
sound.update(s.accTime)
|
||||||
|
|
||||||
|
// Clean up finished non-looping effects
|
||||||
|
if !sound.effect.IsPlaying() {
|
||||||
|
delete(s.sounds, sound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up stopped looping effects
|
||||||
|
if sound.state == envStopped {
|
||||||
|
delete(s.sounds, sound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.timer = 0.2
|
||||||
|
s.accTime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset stop all sounds and reset state
|
||||||
|
func (s *SoundEngine) Reset() {
|
||||||
|
for snd := range s.sounds {
|
||||||
|
snd.effect.Stop()
|
||||||
|
delete(s.sounds, snd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlaySoundID plays a sound by sounds.txt index, returning the sound here is kinda ugly
|
||||||
|
// now we could have a situation where someone holds onto the sound after the sound engine is done with it
|
||||||
|
// someone needs to be in charge of deciding when to stopping looping sounds though...
|
||||||
|
func (s *SoundEngine) PlaySoundID(id int) *Sound {
|
||||||
|
if id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := d2datadict.SelectSoundByIndex(id)
|
||||||
|
|
||||||
|
if entry.GroupSize > 0 {
|
||||||
|
entry = d2datadict.SelectSoundByIndex(entry.Index + rand.Intn(entry.GroupSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
effect, _ := s.provider.LoadSound(entry.FileName, entry.Loop, entry.MusicVol)
|
||||||
|
|
||||||
|
snd := Sound{
|
||||||
|
entry: entry,
|
||||||
|
effect: effect,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.sounds[&snd] = struct{}{}
|
||||||
|
|
||||||
|
snd.Play()
|
||||||
|
|
||||||
|
return &snd
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlaySoundHandle plays a sound by sounds.txt handle
|
||||||
|
func (s *SoundEngine) PlaySoundHandle(handle string) *Sound {
|
||||||
|
sound := d2datadict.Sounds[handle].Index
|
||||||
|
return s.PlaySoundID(sound)
|
||||||
|
}
|
55
d2core/d2audio/sound_environment.go
Normal file
55
d2core/d2audio/sound_environment.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package d2audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SoundEnvironment struct {
|
||||||
|
environment *d2datadict.SoundEnvironRecord
|
||||||
|
engine *SoundEngine
|
||||||
|
bgm *Sound
|
||||||
|
ambiance *Sound
|
||||||
|
eventTimer float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSoundEnvironment(soundEngine *SoundEngine) SoundEnvironment {
|
||||||
|
r := SoundEnvironment{
|
||||||
|
// Start with env NONE
|
||||||
|
environment: d2datadict.SoundEnvirons[0],
|
||||||
|
engine: soundEngine,
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SoundEnvironment) SetEnv(environment int) {
|
||||||
|
if s.environment.Index != environment {
|
||||||
|
newEnv := d2datadict.SoundEnvirons[environment]
|
||||||
|
|
||||||
|
if s.environment.Song != newEnv.Song {
|
||||||
|
if s.bgm != nil {
|
||||||
|
s.bgm.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.bgm = s.engine.PlaySoundID(newEnv.Song)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.environment.DayAmbience != newEnv.DayAmbience {
|
||||||
|
if s.ambiance != nil {
|
||||||
|
s.ambiance.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ambiance = s.engine.PlaySoundID(newEnv.DayAmbience)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.environment = newEnv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SoundEnvironment) Advance(elapsed float64) {
|
||||||
|
s.eventTimer -= elapsed
|
||||||
|
if s.eventTimer < 0 {
|
||||||
|
s.eventTimer = float64(s.environment.EventDelay) / 25
|
||||||
|
s.engine.PlaySoundID(s.environment.DayEvent)
|
||||||
|
}
|
||||||
|
}
|
@ -33,7 +33,7 @@ type UI struct {
|
|||||||
var singleton UI
|
var singleton UI
|
||||||
|
|
||||||
func Initialize(inputManager d2interface.InputManager, audioProvider d2interface.AudioProvider) {
|
func Initialize(inputManager d2interface.InputManager, audioProvider d2interface.AudioProvider) {
|
||||||
sfx, err := audioProvider.LoadSoundEffect(d2resource.SFXButtonClick)
|
sfx, err := audioProvider.LoadSound(d2resource.SFXButtonClick, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to initialize ui: %v", err)
|
log.Fatalf("failed to initialize ui: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -319,7 +319,7 @@ func (m *EscapeMenu) addEnumLabel(l *layout, optID optionID, text string, values
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *EscapeMenu) onLoad() {
|
func (m *EscapeMenu) onLoad() {
|
||||||
m.selectSound, _ = m.audioProvider.LoadSoundEffect(d2resource.SFXCursorSelect)
|
m.selectSound, _ = m.audioProvider.LoadSound(d2resource.SFXCursorSelect, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *EscapeMenu) onEscKey() {
|
func (m *EscapeMenu) onEscKey() {
|
||||||
|
@ -2,14 +2,13 @@ package d2gamescreen
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
|
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer"
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
|
||||||
@ -36,6 +35,8 @@ type Game struct {
|
|||||||
lastRegionType d2enum.RegionIdType
|
lastRegionType d2enum.RegionIdType
|
||||||
ticksSinceLevelCheck float64
|
ticksSinceLevelCheck float64
|
||||||
escapeMenu *EscapeMenu
|
escapeMenu *EscapeMenu
|
||||||
|
soundEngine *d2audio.SoundEngine
|
||||||
|
soundEnv d2audio.SoundEnvironment
|
||||||
|
|
||||||
renderer d2interface.Renderer
|
renderer d2interface.Renderer
|
||||||
inputManager d2interface.InputManager
|
inputManager d2interface.InputManager
|
||||||
@ -75,7 +76,10 @@ func CreateGame(
|
|||||||
audioProvider: audioProvider,
|
audioProvider: audioProvider,
|
||||||
renderer: renderer,
|
renderer: renderer,
|
||||||
terminal: term,
|
terminal: term,
|
||||||
|
soundEngine: d2audio.NewSoundEngine(audioProvider, term),
|
||||||
}
|
}
|
||||||
|
result.soundEnv = d2audio.NewSoundEnvironment(result.soundEngine)
|
||||||
|
|
||||||
result.escapeMenu.onLoad()
|
result.escapeMenu.onLoad()
|
||||||
|
|
||||||
if err := inputManager.BindHandler(result.escapeMenu); err != nil {
|
if err := inputManager.BindHandler(result.escapeMenu); err != nil {
|
||||||
@ -125,6 +129,8 @@ func (v *Game) OnUnload() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v.soundEngine.Reset()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,6 +158,8 @@ func (v *Game) Render(screen d2interface.Surface) error {
|
|||||||
|
|
||||||
// Advance runs the update logic on the Gameplay screen
|
// Advance runs the update logic on the Gameplay screen
|
||||||
func (v *Game) Advance(elapsed float64) error {
|
func (v *Game) Advance(elapsed float64) error {
|
||||||
|
v.soundEngine.Advance(elapsed)
|
||||||
|
|
||||||
if (v.escapeMenu != nil && !v.escapeMenu.isOpen) || len(v.gameClient.Players) != 1 {
|
if (v.escapeMenu != nil && !v.escapeMenu.isOpen) || len(v.gameClient.Players) != 1 {
|
||||||
v.gameClient.MapEngine.Advance(elapsed) // TODO: Hack
|
v.gameClient.MapEngine.Advance(elapsed) // TODO: Hack
|
||||||
}
|
}
|
||||||
@ -170,8 +178,7 @@ func (v *Game) Advance(elapsed float64) error {
|
|||||||
tile := v.gameClient.MapEngine.TileAt(int(tilePosition.X()), int(tilePosition.Y()))
|
tile := v.gameClient.MapEngine.TileAt(int(tilePosition.X()), int(tilePosition.Y()))
|
||||||
|
|
||||||
if tile != nil {
|
if tile != nil {
|
||||||
musicInfo := d2common.GetMusicDef(tile.RegionType)
|
v.soundEnv.SetEnv(d2datadict.LevelDetails[int(tile.RegionType)].SoundEnvironmentID)
|
||||||
v.audioProvider.PlayBGM(musicInfo.MusicFile)
|
|
||||||
|
|
||||||
// skip showing zone change text the first time we enter the world
|
// skip showing zone change text the first time we enter the world
|
||||||
if v.lastRegionType != d2enum.RegionNone && v.lastRegionType != tile.RegionType {
|
if v.lastRegionType != d2enum.RegionNone && v.lastRegionType != tile.RegionType {
|
||||||
@ -203,6 +210,8 @@ func (v *Game) Advance(elapsed float64) error {
|
|||||||
v.mapRenderer.SetCameraTarget(&position)
|
v.mapRenderer.SetCameraTarget(&position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v.soundEnv.Advance(elapsed)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -755,6 +755,6 @@ func loadSprite(animationPath string, position image.Point, playLength int, play
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *SelectHeroClass) loadSoundEffect(sfx string) d2interface.SoundEffect {
|
func (v *SelectHeroClass) loadSoundEffect(sfx string) d2interface.SoundEffect {
|
||||||
result, _ := v.audioProvider.LoadSoundEffect(sfx)
|
result, _ := v.audioProvider.LoadSound(sfx, false, false)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user