ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 8m33s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 12m38s
ci / release (push) Successful in 9m25s
make web-bench measures the exported platformer in Chromium and Firefox in lockstep, with exact WebGL counters against a per-mode baseline, time compared with Mann-Whitney and a profile pass that gates allocations in the engine frame to zero, except listed first-decode and growth sites. On the web the frame loop fills the audio output, the stream feeder waits for ring space, per-event allocations are gone and Go allocations per frame drop from 7.2 to 3.8 in Chromium. The pixel ratio is capped at 1.5 by default (display.maxPixelRatio) and Frame Limit works on the web. The game loop stays on the main thread; a Worker is not built (D391).
885 lines
18 KiB
Go
885 lines
18 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
|
|
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
|
|
}
|
|
|
|
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 {
|
|
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))
|
|
})
|
|
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)
|
|
}
|
|
}
|