Files

905 lines
19 KiB
Go

package audio
import (
"slices"
"aego/asset"
"aego/audio/mix"
"aego/contracts/camera2d"
"aego/contracts/scriptwait"
"aego/contracts/signals"
"aego/core/clock"
"aego/core/ids"
"aego/core/mathx"
"aego/core/rng"
"aego/data"
"aego/engine"
"aego/engine/recorder"
"aego/scene"
"aego/script/abi"
"aego/script/vm"
)
const (
ServiceID = ID + ".voices"
SystemReady = ID + ".ready"
SystemWorld = ID + ".world"
ScriptModule = "aego/audio"
KindSound = ScriptModule + ".Sound"
sigCompleted = 0
OwnerFade = 0.05
stealFade = 0.02
musicPriority = 100
epsilon = 1e-3
rngSeed = 0x5eeda0d1
recPlay = "audio.play"
recStop = "audio.stop"
)
var typeSound = abi.HandleID(KindSound)
type Sound uint64
func soundOf(k uint64) Sound {
return Sound(k)
}
func (s Sound) key() uint64 {
return uint64(s)
}
func (s Sound) voice() mix.VoiceID {
return mix.VoiceID{Index: uint32(s), Gen: uint32(s >> 32)}
}
func soundFor(id mix.VoiceID) Sound {
return Sound(uint64(id.Gen)<<32 | uint64(id.Index))
}
type Options struct {
Volume float32
Pitch float32
Pan float32
Bus Bus
Loop bool
FadeIn float32
Priority int64
Unscaled bool
}
type voiceRec struct {
id mix.VoiceID
owner scene.NodeHandle
event ids.AssetID
sp Spatial
at mathx.Vec2
volume float32
clipGain float32
sentG float32
sentP float32
born uint64
cancel chan struct{}
source bool
spatial bool
follow bool
game bool
held bool
user bool
stopped bool
music bool
length float32
}
type zoneState struct {
node scene.NodeHandle
inside bool
seen bool
seq uint64
}
type coolEntry struct {
id ids.AssetID
until float64
}
type playReq struct {
clip ids.AssetID
event ids.AssetID
owner scene.NodeHandle
sp Spatial
at mathx.Vec2
volume float32
pitch float32
pan float32
fade float32
bus int
prio int32
source bool
spatial bool
follow bool
loop bool
game bool
music bool
}
type Service struct {
m *Module
scene *scene.Scene
world *engine.World
hub signals.Hub
rng *rng.Source
recs []voiceRec
zones []zoneState
cool []coolEntry
pending []scene.NodeHandle
worldG [busCount]float32
fades [busCount]float32
music mix.VoiceID
listen mathx.Vec2
// listenerNode records the node selected by the last listener update; a
// zero handle means the camera or default-position fallback was used.
listenerNode scene.NodeHandle
time float64
seq uint64
born uint64
ready bool
paused bool
all bool
lib *data.Library
}
func NewService(m *Module, s *scene.Scene, w *engine.World) *Service {
svc := &Service{m: m, scene: s, world: w, rng: rng.New(rngSeed)}
for b := range svc.worldG {
svc.worldG[b] = 1
}
if w != nil {
svc.hub, _ = engine.ServiceOf[signals.Hub](w, signals.ServiceID)
}
return svc
}
func Of(w *engine.World) *Service {
if w == nil {
return nil
}
s, _ := engine.ServiceOf[*Service](w, ServiceID)
return s
}
func (s *Service) mixer() *mix.Mixer {
if s.m == nil {
return nil
}
return s.m.mixer
}
func (s *Service) replaying() bool {
return s.world.Replaying()
}
func (s *Service) find(id mix.VoiceID) int {
if !id.Valid() {
return -1
}
for i := range s.recs {
if s.recs[i].id == id {
return i
}
}
return -1
}
func (s *Service) alive(key uint64) bool {
return s.find(Sound(key).voice()) >= 0
}
func translation(m mathx.Mat3) mathx.Vec2 {
return mathx.Vec2{X: m[6], Y: m[7]}
}
func (s *Service) posOf(owner scene.NodeHandle, at mathx.Vec2, follow bool) mathx.Vec2 {
if follow && !owner.IsZero() && s.scene.IsAlive(owner) {
return translation(s.scene.RenderWorld(owner))
}
return at
}
type VoiceInfo struct {
Position mathx.Vec2
Gain float32
Spatial bool
Source bool
Owner scene.NodeHandle
}
func (s *Service) Scene() *scene.Scene {
return s.scene
}
func (s *Service) Listener() mathx.Vec2 {
return s.listen
}
// ListenerNode returns the actual selected listener, not an inferred position.
func (s *Service) ListenerNode() scene.NodeHandle {
return s.listenerNode
}
// WorldBusGain returns the last zone-selected world-layer target. This is not
// the ramped, combined mixer output, which also includes session and settings.
// A missing world or invalid bus has the neutral target 1.
func (s *Service) WorldBusGain(bus Bus) float32 {
if s == nil || int(bus) >= len(s.worldG) {
return 1
}
return s.worldG[bus]
}
func (s *Service) EachVoice(fn func(VoiceInfo)) {
for i := range s.recs {
r := &s.recs[i]
if r.stopped {
continue
}
fn(VoiceInfo{Position: s.posOf(r.owner, r.at, r.follow), Gain: r.sentG, Spatial: r.spatial, Source: r.source, Owner: r.owner})
}
}
func (s *Service) SourceLength(h scene.NodeHandle) float32 {
for i := range s.recs {
if r := &s.recs[i]; r.source && r.owner == h && !r.stopped {
return r.length
}
}
return 0
}
func (s *Service) resolver() asset.Resolver {
if s.scene == nil {
return nil
}
return s.scene.Resolver()
}
func (s *Service) record(name string, owner scene.NodeHandle) {
if s.world == nil || s.world.Recorder == nil {
return
}
var id ids.NodeID
if !owner.IsZero() && s.scene != nil && s.scene.IsAlive(owner) {
id = s.scene.ID(owner)
}
s.world.Recorder.Add(recorder.Audio, id, ids.NodeID{}, name, "")
}
func (s *Service) start(r *playReq) Sound {
mx := s.mixer()
if mx == nil || r.clip.IsZero() {
return 0
}
s.record(recPlay, r.owner)
if s.replaying() {
return 0
}
c, err := s.m.clip(s.resolver(), r.clip)
if err != nil {
s.m.warnAsset(r.clip, err)
return 0
}
bus := r.bus
if bus < 0 || bus >= busCount {
bus = c.Bus
}
vol := r.volume * c.Gain
gain, pan := vol, r.pan
if r.spatial {
g, p := r.sp.At(s.listen, s.posOf(r.owner, r.at, r.follow))
gain, pan = vol*g, p
}
p := mix.PlayParams{Bus: bus, Gain: gain, Pan: pan, Pitch: r.pitch, Priority: r.prio, Loop: r.loop, FadeIn: r.fade}
var cancel chan struct{}
if c.Mem != nil {
p.Memory = c.Mem
} else {
st, cn, err := c.stream(r.loop)
if err != nil {
s.m.warnAsset(r.clip, err)
return 0
}
p.Stream, cancel = st, cn
}
id := mx.Play(p)
if !id.Valid() {
if cancel != nil {
close(cancel)
}
return 0
}
s.born++
rec := voiceRec{
id: id, owner: r.owner, event: r.event, sp: r.sp, at: r.at, volume: vol, clipGain: c.Gain,
sentG: gain, sentP: pan, born: s.born, cancel: cancel, source: r.source, spatial: r.spatial,
follow: r.follow, game: r.game, music: r.music, length: c.Seconds,
}
if s.wantPause(&rec) {
rec.held = true
mx.Pause(id)
}
s.recs = append(s.recs, rec)
return soundFor(id)
}
func (s *Service) request(clip ids.AssetID, o Options, owner scene.NodeHandle) playReq {
vol, pitch := o.Volume, o.Pitch
if vol == 0 {
vol = 1
}
if pitch == 0 {
pitch = 1
}
bus := int(o.Bus)
if o.Bus == BusMaster {
bus = -1
}
return playReq{
clip: clip, owner: owner, volume: vol, pitch: pitch, pan: o.Pan, bus: bus, loop: o.Loop,
fade: o.FadeIn, prio: int32(min(max(o.Priority, 0), 255)), game: !o.Unscaled,
}
}
func (s *Service) Play(clip ids.AssetID, o Options, owner scene.NodeHandle) Sound {
r := s.request(clip, o, owner)
return s.start(&r)
}
func (s *Service) PlayAt(clip ids.AssetID, pos mathx.Vec2, o Options, owner scene.NodeHandle) Sound {
r := s.request(clip, o, owner)
r.spatial, r.at, r.sp = true, pos, defaultSpatial
return s.start(&r)
}
func (s *Service) PlayEvent(ev ids.AssetID, owner scene.NodeHandle) Sound {
return s.playEvent(ev, owner, mathx.Vec2{}, false, false)
}
func (s *Service) PlayEventAt(ev ids.AssetID, pos mathx.Vec2, owner scene.NodeHandle) Sound {
return s.playEvent(ev, owner, pos, true, false)
}
func (s *Service) library() (*data.Library, error) {
if s.lib == nil {
src, ok := s.scene.Resolver().(asset.DataSource)
if !ok {
return nil, data.ErrNoSource
}
s.lib = data.NewLibrary(s.scene.Registry(), src)
}
return s.lib, nil
}
func (s *Service) event(id ids.AssetID) (*Event, error) {
l, err := s.library()
if err != nil {
return nil, err
}
return data.Get[Event](l, id)
}
func (s *Service) playEvent(id ids.AssetID, owner scene.NodeHandle, at mathx.Vec2, positioned, source bool) Sound {
ev, err := s.event(id)
if err != nil {
s.m.warnAsset(id, err)
return 0
}
return s.playResolved(id, ev, owner, at, positioned, source)
}
func (s *Service) Trigger(key ids.AssetID, ev *Event, owner scene.NodeHandle) Sound {
if ev == nil {
return 0
}
return s.playResolved(key, ev, owner, mathx.Vec2{}, false, false)
}
func (s *Service) TriggerAt(key ids.AssetID, ev *Event, pos mathx.Vec2, owner scene.NodeHandle) Sound {
if ev == nil {
return 0
}
return s.playResolved(key, ev, owner, pos, true, false)
}
func (s *Service) playResolved(id ids.AssetID, ev *Event, owner scene.NodeHandle, at mathx.Vec2, positioned, source bool) Sound {
if !s.allow(id, ev) {
return 0
}
clip, ok := s.variant(ev)
if !ok {
return 0
}
vol := ev.Volume * (1 - ev.VolumeRand*s.rng.Float32())
pitch := ev.Pitch * (1 + ev.PitchRand*(2*s.rng.Float32()-1))
r := playReq{
clip: clip, event: id, owner: owner, at: at, volume: vol, pitch: pitch, bus: int(ev.Bus), loop: ev.Loop,
prio: ev.Priority, source: source, game: ev.PauseWith == PauseGame, spatial: ev.Spatial, follow: !positioned,
sp: Spatial{Min: ev.MinDistance, Max: ev.MaxDistance, Width: ev.PanWidth, Rolloff: ev.Rolloff},
}
return s.start(&r)
}
func (s *Service) variant(ev *Event) (ids.AssetID, bool) {
n := 0
for _, v := range ev.Variants {
if !v.IsZero() {
n++
}
}
if n == 0 {
return ids.AssetID{}, false
}
k := min(int(s.rng.Float32()*float32(n)), n-1)
for _, v := range ev.Variants {
if v.IsZero() {
continue
}
if k == 0 {
return v.ID, true
}
k--
}
return ids.AssetID{}, false
}
func (s *Service) coolIndex(id ids.AssetID) int {
for i := range s.cool {
if s.cool[i].id == id {
return i
}
}
return -1
}
func (s *Service) allow(id ids.AssetID, ev *Event) bool {
if ev.Cooldown > 0 {
until := s.time + float64(ev.Cooldown)
switch i := s.coolIndex(id); {
case i >= 0 && s.time < s.cool[i].until:
return false
case i >= 0:
s.cool[i].until = until
default:
s.cool = append(s.cool, coolEntry{id: id, until: until})
}
}
if ev.MaxVoices <= 0 {
return true
}
for {
n, victim := 0, -1
mx := s.mixer()
for i := range s.recs {
r := &s.recs[i]
if r.event != id || r.stopped {
continue
}
n++
switch {
case victim < 0:
victim = i
case ev.Steal == StealQuietest && mx != nil && mx.VoiceGain(r.id) < mx.VoiceGain(s.recs[victim].id):
victim = i
case ev.Steal != StealQuietest && r.born < s.recs[victim].born:
victim = i
}
}
if n < int(ev.MaxVoices) {
return true
}
if ev.Steal == StealNone || victim < 0 {
return false
}
s.stopRec(victim, stealFade)
}
}
func (s *Service) Music(clip ids.AssetID, fade float32) Sound {
s.StopMusic(fade)
r := playReq{clip: clip, volume: 1, pitch: 1, bus: int(BusMusic), loop: true, fade: fade, prio: musicPriority, music: true}
snd := s.start(&r)
s.music = snd.voice()
return snd
}
func (s *Service) StopMusic(fade float32) {
if i := s.find(s.music); i >= 0 {
s.stopRec(i, fade)
}
s.music = mix.VoiceID{}
}
func (s *Service) stopRec(i int, fade float32) {
r := &s.recs[i]
if r.stopped {
return
}
r.stopped = true
s.record(recStop, r.owner)
if mx := s.mixer(); mx != nil {
mx.Stop(r.id, fade)
}
}
func (s *Service) Stop(h Sound, fade float32) {
if i := s.find(h.voice()); i >= 0 {
s.stopRec(i, fade)
}
}
func (s *Service) Pause(h Sound, on bool) {
if i := s.find(h.voice()); i >= 0 {
s.recs[i].user = on
s.syncPause(&s.recs[i])
}
}
func (s *Service) SetVolume(h Sound, v float32) {
i := s.find(h.voice())
if i < 0 {
return
}
r := &s.recs[i]
r.volume = max(v, 0) * r.clipGain
if mx := s.mixer(); mx != nil && !r.spatial && !r.stopped {
mx.SetGain(r.id, r.volume, 0)
r.sentG = r.volume
}
}
func (s *Service) SetPitch(h Sound, p float32) {
if i := s.find(h.voice()); i >= 0 {
if mx := s.mixer(); mx != nil {
mx.SetPitch(s.recs[i].id, p)
}
}
}
func (s *Service) SetPan(h Sound, p float32) {
i := s.find(h.voice())
if i < 0 || s.recs[i].spatial {
return
}
if mx := s.mixer(); mx != nil {
mx.SetPan(s.recs[i].id, p)
s.recs[i].sentP = p
}
}
func (s *Service) Done(h Sound) bool {
return s.find(h.voice()) < 0
}
func (s *Service) Playing(h Sound) bool {
i := s.find(h.voice())
return i >= 0 && !s.recs[i].stopped && !s.recs[i].held
}
func (s *Service) Position(h Sound) float32 {
if mx := s.mixer(); mx != nil && s.find(h.voice()) >= 0 {
return mx.Position(h.voice())
}
return 0
}
func (s *Service) PauseAll(on bool) {
s.all = on
for i := range s.recs {
s.syncPause(&s.recs[i])
}
}
func (s *Service) Voices() int {
return len(s.recs)
}
func (s *Service) PlaySource(h scene.NodeHandle) Sound {
c, ok := scene.Get[AudioSource2D](s.scene, h)
if !ok {
return 0
}
s.StopSource(h, stealFade)
var snd Sound
if !c.Event.IsZero() {
snd = s.playEvent(c.Event.ID, h, mathx.Vec2{}, false, true)
} else {
r := playReq{
clip: c.Clip.ID, owner: h, source: true, volume: c.Volume, pitch: c.Pitch, pan: c.Pan, bus: int(c.Bus),
loop: c.Loop, game: c.PauseWith == PauseGame, spatial: c.Spatial, follow: true,
sp: Spatial{Min: c.MinDistance, Max: c.MaxDistance, Width: c.PanWidth, Rolloff: c.Rolloff},
}
snd = s.start(&r)
}
if snd == 0 {
return 0
}
c.Playing, c.Position = true, 0
s.m.started.Emit(s.world, h)
return snd
}
func (s *Service) StopSource(h scene.NodeHandle, fade float32) {
for i := range s.recs {
if r := &s.recs[i]; r.source && r.owner == h {
s.stopRec(i, fade)
}
}
}
func (s *Service) SourcePlaying(h scene.NodeHandle) bool {
for i := range s.recs {
if r := &s.recs[i]; r.source && r.owner == h && !r.stopped {
return true
}
}
return false
}
func (s *Service) wantPause(r *voiceRec) bool {
return r.user || s.all || (r.game && s.paused)
}
func (s *Service) syncPause(r *voiceRec) {
want := s.wantPause(r)
if want == r.held || r.stopped {
return
}
r.held = want
if mx := s.mixer(); mx != nil {
if want {
mx.Pause(r.id)
} else {
mx.Resume(r.id)
}
}
}
func (s *Service) startOnPlay() {
s.pending = s.pending[:0]
scene.Query1[AudioSource2D](s.scene).Each(func(h scene.NodeHandle, c *AudioSource2D) {
if c.PlayOnStart && s.scene.EffectiveEnabled(h) {
s.pending = append(s.pending, h)
}
})
for _, h := range s.pending {
s.PlaySource(h)
}
}
func (s *Service) listener() mathx.Vec2 {
s.listenerNode = scene.NodeHandle{}
found := false
var pos mathx.Vec2
scene.Query1[AudioListener2D](s.scene).Each(func(h scene.NodeHandle, l *AudioListener2D) {
if found || !l.Active || !s.scene.EffectiveEnabled(h) {
return
}
found, pos = true, translation(s.scene.RenderWorld(h))
s.listenerNode = h
})
if found {
return pos
}
if v, ok := engine.ServiceOf[camera2d.Viewer](s.world, camera2d.ServiceID); ok {
if p, ok := v.ViewCenter(); ok {
return p
}
}
return mathx.Vec2{}
}
func (s *Service) zoneFor(h scene.NodeHandle) *zoneState {
for i := range s.zones {
if s.zones[i].node == h {
return &s.zones[i]
}
}
s.zones = append(s.zones, zoneState{node: h})
return &s.zones[len(s.zones)-1]
}
func (s *Service) updateZones() {
for i := range s.zones {
s.zones[i].seen = false
}
scene.Query1[AudioZone2D](s.scene).Each(func(h scene.NodeHandle, z *AudioZone2D) {
in := false
if s.scene.EffectiveEnabled(h) && z.Area.W > 0 && z.Area.H > 0 {
if inv, ok := s.scene.World(h).Inverse(); ok {
p, a := inv.Apply(s.listen), z.Area
in = p.X >= a.X && p.X <= a.X+a.W && p.Y >= a.Y && p.Y <= a.Y+a.H
}
}
st := s.zoneFor(h)
st.seen = true
if in && !st.inside {
s.seq++
st.seq = s.seq
}
st.inside = in
})
n := 0
for _, z := range s.zones {
if z.seen {
s.zones[n] = z
n++
}
}
s.zones = s.zones[:n]
s.applyZones()
}
func (s *Service) applyZones() {
mx := s.mixer()
for b := 0; b < busCount; b++ {
target := float32(1)
var best *AudioZone2D
var bestSeq uint64
for i := range s.zones {
z := &s.zones[i]
if !z.inside {
continue
}
c, ok := scene.Get[AudioZone2D](s.scene, z.node)
if !ok || int(c.Bus) != b {
continue
}
if best == nil || c.Priority > best.Priority || (c.Priority == best.Priority && z.seq > bestSeq) {
best, bestSeq = c, z.seq
}
}
if best != nil {
target, s.fades[b] = best.Gain, best.FadeTime
}
if target == s.worldG[b] || mx == nil || s.replaying() {
continue
}
s.worldG[b] = target
mx.SetBusLayer(b, mix.LayerWorld, target, s.fades[b])
}
}
func (s *Service) update(ctx *engine.Context) {
if t := ctx.Time; t != nil {
s.time += t.Span(clock.Game).Delta
s.paused = t.Paused()
}
s.listen = s.listener()
s.updateZones()
mx := s.mixer()
if mx == nil {
return
}
for i := 0; i < len(s.recs); i++ {
r := &s.recs[i]
if r.stopped {
continue
}
if !r.owner.IsZero() && !s.scene.IsAlive(r.owner) {
s.stopRec(i, OwnerFade)
continue
}
s.syncPause(r)
if r.spatial {
g, p := r.sp.At(s.listen, s.posOf(r.owner, r.at, r.follow))
g *= r.volume
if mathx.Abs(g-r.sentG) > epsilon {
mx.SetGain(r.id, g, 0)
r.sentG = g
}
if mathx.Abs(p-r.sentP) > epsilon {
mx.SetPan(r.id, p)
r.sentP = p
}
}
if r.source {
if c, ok := scene.Get[AudioSource2D](s.scene, r.owner); ok {
c.Playing, c.Position = true, mx.Position(r.id)
}
}
}
}
func (s *Service) complete(key uint64, finished bool) {
src := signals.Source{Kind: signals.KindHandle, A: key, B: typeSound}
if s.hub != nil {
if s.hub.Wants(src) {
args := s.hub.Args(1)
args[0] = vm.Bool(finished)
s.hub.Emit(src, sigCompleted, args)
}
s.hub.End(src)
}
if w, ok := engine.ServiceOf[scriptwait.Waker](s.world, scriptwait.ServiceID); ok {
w.Complete(typeSound, key)
}
}
func (s *Service) finish(r *voiceRec, finished bool) {
if r.cancel != nil {
close(r.cancel)
r.cancel = nil
}
if r.music && s.music == r.id {
s.music = mix.VoiceID{}
}
if r.source && s.scene.IsAlive(r.owner) {
if !s.SourcePlaying(r.owner) {
if c, ok := scene.Get[AudioSource2D](s.scene, r.owner); ok {
c.Playing = false
}
}
s.m.finished.Emit(s.world, r.owner, finished)
}
s.complete(soundFor(r.id).key(), finished)
}
func (s *Service) onEvent(ev mix.Event) bool {
i := s.find(ev.Voice)
if i < 0 {
return false
}
if ev.Kind == mix.EvStarved {
return true
}
r := s.recs[i]
s.recs = slices.Delete(s.recs, i, i+1)
s.finish(&r, ev.Kind == mix.EvFinished && !r.stopped)
return true
}
func (s *Service) drop(kill bool, fade float32) {
mx := s.mixer()
recs := s.recs
s.recs = nil
for i := range recs {
r := &recs[i]
if mx != nil {
if kill {
mx.Kill(r.id)
} else if !r.stopped {
mx.Stop(r.id, fade)
}
}
s.finish(r, false)
}
s.music = mix.VoiceID{}
}
func (s *Service) close() {
s.drop(false, OwnerFade)
if mx := s.mixer(); mx != nil {
for b := range s.worldG {
if s.worldG[b] != 1 {
mx.SetBusLayer(b, mix.LayerWorld, 1, OwnerFade)
s.worldG[b] = 1
}
}
}
}
type readySystem struct{}
func (readySystem) Run(ctx *engine.Context) {
if s := Of(ctx.World); s != nil && !s.ready {
s.ready = true
s.startOnPlay()
}
}
type worldSystem struct{}
func (worldSystem) Run(ctx *engine.Context) {
if s := Of(ctx.World); s != nil {
s.update(ctx)
}
}