Files
aego/audio/mix/mix.go
T
sanspie 72d9a21782
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
Stage 12w: web runtime performance (D365, D366, D390, D391)
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).
2026-10-27 16:29:41 +03:00

382 lines
8.0 KiB
Go

package mix
import (
"errors"
"math"
"slices"
"sync/atomic"
)
const (
DefaultVoices = 64
DefaultBlock = 512
MaxBlock = 4096
MinPitch = 0.25
MaxPitch = 4
MaxRateRatio = 4
queueSize = 1024
gainRamp = 0.010
busRamp = 0.010
)
type BusDesc struct {
ID string
Parent string
}
type Config struct {
SampleRate int
Channels int
Voices int
Buses []BusDesc
Block int
}
type VoiceID struct {
Index uint32
Gen uint32
}
func (v VoiceID) Valid() bool {
return v.Gen != 0
}
type PlayParams struct {
Memory *Memory
Stream *Stream
Bus int
Gain float32
Pan float32
Pitch float32
Priority int32
Loop bool
FadeIn float32
}
const (
LayerUser uint8 = iota
LayerSession
LayerWorld
)
type handle struct {
gen atomic.Uint32
pos atomic.Uint32
gain atomic.Uint32
voice int32
}
type bus struct {
id string
parent int
depth int
user float32
session float32
world float32
mute bool
solo bool
audible bool
gain float32
target float32
delta float32
ramp int
live bool
acc []float32
peak atomic.Uint32
rms atomic.Uint32
level atomic.Uint32
}
type Mixer struct {
cfg Config
rate float64
srate atomic.Int32
buses []bus
order []int
byID map[string]int
master int
voices []voice
freeV []int32
handles []handle
hfree *spsc[uint32]
spare []uint32
cmds CommandQueue
events EventQueue
paused bool
clock uint64
ages uint64
applied uint64
active atomic.Int32
refused atomic.Uint64
}
func New(cfg Config) (*Mixer, error) {
switch {
case cfg.SampleRate <= 0:
return nil, errors.New("mix: the sample rate must be positive")
case cfg.Channels != 0 && cfg.Channels != 2:
return nil, errors.New("mix: the output must be stereo")
}
cfg.Channels = 2
if cfg.Voices <= 0 {
cfg.Voices = DefaultVoices
}
if cfg.Block <= 0 {
cfg.Block = DefaultBlock
}
cfg.Block = min(cfg.Block, MaxBlock)
if len(cfg.Buses) == 0 {
cfg.Buses = []BusDesc{{ID: "master"}}
}
m := &Mixer{cfg: cfg, rate: float64(cfg.SampleRate)}
m.srate.Store(int32(cfg.SampleRate))
if err := m.initBuses(cfg.Buses); err != nil {
return nil, err
}
m.voices = make([]voice, cfg.Voices)
m.freeV = make([]int32, 0, cfg.Voices)
for i := cfg.Voices - 1; i >= 0; i-- {
m.freeV = append(m.freeV, int32(i))
}
n := 1
for n < cfg.Voices*4 {
n <<= 1
}
m.handles = make([]handle, n)
m.hfree = newSPSC[uint32](n)
for i := range m.handles {
m.handles[i].gen.Store(1)
m.handles[i].voice = -1
m.hfree.push(uint32(i))
}
m.spare = make([]uint32, 0, n)
m.cmds.q = newSPSC[Command](queueSize)
m.cmds.wake = make(chan struct{}, 1)
m.events.q = newSPSC[Event](queueSize)
return m, nil
}
func (m *Mixer) initBuses(descs []BusDesc) error {
m.byID = make(map[string]int, len(descs))
for i, d := range descs {
if d.ID == "" {
return errors.New("mix: a bus needs an id")
}
if _, dup := m.byID[d.ID]; dup {
return errors.New("mix: bus " + d.ID + " is declared twice")
}
m.byID[d.ID] = i
}
m.buses = make([]bus, len(descs))
m.master = -1
for i, d := range descs {
b := &m.buses[i]
b.id, b.parent, b.user, b.session, b.world, b.gain, b.target, b.audible = d.ID, -1, 1, 1, 1, 1, 1, true
b.acc = make([]float32, m.cfg.Block*2)
b.level.Store(math.Float32bits(1))
if d.Parent == "" {
if m.master >= 0 {
return errors.New("mix: buses " + descs[m.master].ID + " and " + d.ID + " both have no parent")
}
m.master = i
continue
}
p, ok := m.byID[d.Parent]
if !ok {
return errors.New("mix: bus " + d.ID + " has an unknown parent " + d.Parent)
}
b.parent = p
}
if m.master < 0 {
return errors.New("mix: there is no master bus without a parent")
}
for i := range m.buses {
depth := 0
for j := m.buses[i].parent; j >= 0; j = m.buses[j].parent {
depth++
if depth > len(m.buses) {
return errors.New("mix: bus " + descs[i].ID + " is part of a cycle")
}
}
m.buses[i].depth = depth
}
m.order = make([]int, len(m.buses))
for i := range m.order {
m.order[i] = i
}
slices.SortStableFunc(m.order, func(a, b int) int { return m.buses[b].depth - m.buses[a].depth })
return nil
}
func (m *Mixer) Reserve() VoiceID {
if n := len(m.spare); n > 0 {
h := m.spare[n-1]
m.spare = m.spare[:n-1]
return VoiceID{Index: h, Gen: m.handles[h].gen.Load()}
}
h, ok := m.hfree.pop()
if !ok {
m.refused.Add(1)
return VoiceID{}
}
return VoiceID{Index: h, Gen: m.handles[h].gen.Load()}
}
func (m *Mixer) Play(p PlayParams) VoiceID {
id := m.Reserve()
if !id.Valid() {
return id
}
pitch := p.Pitch
if pitch == 0 {
pitch = 1
}
ok := m.cmds.Push(Command{
Kind: CmdPlay, Voice: id, Bus: int32(p.Bus), Memory: p.Memory, Stream: p.Stream,
Gain: p.Gain, Pan: p.Pan, Pitch: pitch, Fade: p.FadeIn, Priority: p.Priority, Flag: p.Loop,
})
if !ok {
m.spare = append(m.spare, id.Index)
return VoiceID{}
}
return id
}
func (m *Mixer) Stop(id VoiceID, fade float32) bool {
return m.cmds.Push(Command{Kind: CmdStop, Voice: id, Fade: fade})
}
func (m *Mixer) SetGain(id VoiceID, gain, fade float32) bool {
return m.cmds.Push(Command{Kind: CmdSetGain, Voice: id, Gain: gain, Fade: fade})
}
func (m *Mixer) SetPan(id VoiceID, pan float32) bool {
return m.cmds.Push(Command{Kind: CmdSetPan, Voice: id, Pan: pan})
}
func (m *Mixer) SetPitch(id VoiceID, pitch float32) bool {
return m.cmds.Push(Command{Kind: CmdSetPitch, Voice: id, Pitch: pitch})
}
func (m *Mixer) Pause(id VoiceID) bool {
return m.cmds.Push(Command{Kind: CmdPause, Voice: id})
}
func (m *Mixer) Resume(id VoiceID) bool {
return m.cmds.Push(Command{Kind: CmdResume, Voice: id})
}
func (m *Mixer) SetBusGain(b int, gain float32) bool {
return m.cmds.Push(Command{Kind: CmdBusGain, Bus: int32(b), Gain: gain})
}
func (m *Mixer) SetBusMute(b int, on bool) bool {
return m.cmds.Push(Command{Kind: CmdBusMute, Bus: int32(b), Flag: on})
}
func (m *Mixer) SetBusSolo(b int, on bool) bool {
return m.cmds.Push(Command{Kind: CmdBusSolo, Bus: int32(b), Flag: on})
}
func (m *Mixer) PauseAll() bool {
return m.cmds.Push(Command{Kind: CmdPauseAll})
}
func (m *Mixer) ResumeAll() bool {
return m.cmds.Push(Command{Kind: CmdResumeAll})
}
func (m *Mixer) SampleRate() int {
return int(m.srate.Load())
}
func (m *Mixer) Block() int {
return m.cfg.Block
}
func (m *Mixer) Bus(id string) (int, bool) {
i, ok := m.byID[id]
return i, ok
}
func (m *Mixer) BusCount() int {
return len(m.buses)
}
func (m *Mixer) Meters(b int) (peak, rms float32) {
if b < 0 || b >= len(m.buses) {
return 0, 0
}
return math.Float32frombits(m.buses[b].peak.Load()), math.Float32frombits(m.buses[b].rms.Load())
}
func (m *Mixer) BusGain(b int) float32 {
if b < 0 || b >= len(m.buses) {
return 0
}
return math.Float32frombits(m.buses[b].level.Load())
}
func (m *Mixer) Active() int {
return int(m.active.Load())
}
func (m *Mixer) Refused() uint64 {
return m.refused.Load()
}
func (m *Mixer) Commands() *CommandQueue {
return &m.cmds
}
func (m *Mixer) Events() *EventQueue {
return &m.events
}
func (m *Mixer) SetSampleRate(rate int) {
if rate <= 0 {
return
}
m.rate = float64(rate)
m.cfg.SampleRate = rate
m.srate.Store(int32(rate))
for i := range m.voices {
v := &m.voices[i]
switch {
case v.state == voiceFree:
continue
case v.mem != nil:
v.ratio = float64(v.mem.Rate) / m.rate
default:
v.ratio = float64(v.st.Rate) / m.rate
}
v.step = float64(v.pitch) * v.ratio
}
}
func (m *Mixer) SetBusLayer(b int, layer uint8, gain, fade float32) bool {
return m.cmds.Push(Command{Kind: CmdBusLayer, Bus: int32(b), Layer: layer, Gain: gain, Fade: fade})
}
func (m *Mixer) Kill(id VoiceID) bool {
return m.cmds.Push(Command{Kind: CmdKill, Voice: id})
}
func (m *Mixer) Live(id VoiceID) bool {
return id.Gen != 0 && int(id.Index) < len(m.handles) && m.handles[id.Index].gen.Load() == id.Gen
}
func (m *Mixer) Position(id VoiceID) float32 {
if !m.Live(id) {
return 0
}
return math.Float32frombits(m.handles[id.Index].pos.Load())
}
func (m *Mixer) VoiceGain(id VoiceID) float32 {
if !m.Live(id) {
return 0
}
return math.Float32frombits(m.handles[id.Index].gain.Load())
}