Files
aego/audio/mix/source.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

181 lines
3.6 KiB
Go

package mix
import "sync/atomic"
const streamCache = 1<<16 + 16
type Memory struct {
PCM []float32
Rate int
Channels int
LoopStart int64
LoopEnd int64
Loop bool
}
func (m *Memory) Frames() int64 {
if m.Channels <= 0 {
return 0
}
return int64(len(m.PCM) / m.Channels)
}
func (m *Memory) loopRange() (int64, int64) {
n := m.Frames()
s, e := m.LoopStart, m.LoopEnd
if e <= 0 || e > n {
e = n
}
if s < 0 || s >= e {
s = 0
}
return s, e
}
type Stream struct {
Ring *Ring
Rate int
Channels int
eof atomic.Bool
skipped atomic.Int64
hold atomic.Bool
wake chan struct{}
space chan struct{}
waiting atomic.Bool
cache []float32
start int64
n int
inUse bool
}
func NewStream(rate, channels, ringFrames int) *Stream {
if channels != 2 {
channels = 1
}
return &Stream{
Ring: NewRing(ringFrames, channels), Rate: rate, Channels: channels,
cache: make([]float32, streamCache*channels), wake: make(chan struct{}, 1), space: make(chan struct{}, 1),
}
}
const spaceShare = 4
// WaitSpace waits until the mixer has read a quarter of the ring, a skip or a hold arrives, or cancel closes; false means cancel.
func (s *Stream) WaitSpace(cancel <-chan struct{}) bool {
s.waiting.Store(true)
defer s.waiting.Store(false)
if s.Ring.Free() >= s.Ring.Cap()/spaceShare {
return true
}
select {
case <-cancel:
return false
case <-s.space:
case <-s.wake:
}
return true
}
func (s *Stream) drained() {
if s.waiting.Load() && s.Ring.Free() >= s.Ring.Cap()/spaceShare {
select {
case s.space <- struct{}{}:
default:
}
}
}
// TakeSkip returns the source frames the feeder must skip before it decodes again, and clears them.
func (s *Stream) TakeSkip() int64 {
return s.skipped.Swap(0)
}
// Held reports that the mixer skips the stream, so the feeder keeps its position and decodes nothing.
func (s *Stream) Held() bool {
return s.hold.Load()
}
// Wake receives when the mixer adds a skip or releases the hold.
func (s *Stream) Wake() <-chan struct{} {
return s.wake
}
func (s *Stream) signal() {
select {
case s.wake <- struct{}{}:
default:
}
}
func (s *Stream) release() {
if s.hold.Swap(false) {
s.signal()
}
}
// skip moves a voice to "to" without rendering: decoded frames are dropped from the cache and the ring, the rest is left to the feeder; it reports a stream that ended.
func (s *Stream) skip(to float64) bool {
ch := s.Channels
want := int64(to) - 1
if want <= s.start {
return false
}
if k := int(min(want-s.start, int64(s.n))); k > 0 {
copy(s.cache, s.cache[k*ch:s.n*ch])
s.n -= k
s.start += int64(k)
}
if rest := want - s.start; rest > 0 {
eof := s.eof.Load()
got := s.Ring.Discard(rest)
s.start += got
if rest -= got; rest > 0 {
if eof && s.Ring.Len() == 0 {
return true
}
s.skipped.Add(rest)
s.start += rest
}
}
s.hold.Store(true)
s.signal()
return false
}
func (s *Stream) SetEOF() {
s.eof.Store(true)
}
func (s *Stream) EOF() bool {
return s.eof.Load()
}
func (s *Stream) reset() {
s.skipped.Store(0)
s.hold.Store(false)
s.start, s.n = -1, 1
clear(s.cache[:s.Channels])
}
func (s *Stream) fill(pos, step float64, frames int) {
ch := s.Channels
if d := int64(pos) - 1 - s.start; d > 0 {
k := int(min(d, int64(s.n)))
copy(s.cache, s.cache[k*ch:s.n*ch])
s.n -= k
s.start += int64(k)
}
need := min(int(int64(pos+step*float64(frames))+3-s.start), streamCache)
if s.n < need {
s.n += s.Ring.Read(s.cache[s.n*ch : need*ch])
s.drained()
}
}
func (s *Stream) at(k, c int) float32 {
if k < 0 || k >= s.n {
return 0
}
return s.cache[k*s.Channels+c]
}