ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Successful in 7m34s
ci / checks (push) Successful in 2m53s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 5m2s
527 lines
10 KiB
Go
527 lines
10 KiB
Go
package mix
|
|
|
|
import (
|
|
"math"
|
|
|
|
"aego/audio/dsp"
|
|
)
|
|
|
|
func (m *Mixer) Mix(out []float32) {
|
|
m.drain()
|
|
for len(out) >= 2 {
|
|
n := min(len(out)/2, m.cfg.Block)
|
|
m.chunk(out[:n*2], n)
|
|
out = out[n*2:]
|
|
}
|
|
}
|
|
|
|
func (m *Mixer) drain() {
|
|
for {
|
|
c, ok := m.cmds.q.pop()
|
|
if !ok {
|
|
return
|
|
}
|
|
m.apply(&c)
|
|
}
|
|
}
|
|
|
|
func (m *Mixer) apply(c *Command) {
|
|
m.applied++
|
|
switch c.Kind {
|
|
case CmdPlay:
|
|
m.play(c)
|
|
case CmdBusGain, CmdBusMute, CmdBusSolo, CmdBusLayer:
|
|
i := int(c.Bus)
|
|
if i < 0 || i >= len(m.buses) {
|
|
return
|
|
}
|
|
b := &m.buses[i]
|
|
switch c.Kind {
|
|
case CmdBusGain:
|
|
b.user = max(c.Gain, 0)
|
|
m.retarget(i, m.seconds(0, busRamp))
|
|
case CmdBusLayer:
|
|
g := max(c.Gain, 0)
|
|
switch c.Layer {
|
|
case LayerSession:
|
|
b.session = g
|
|
case LayerWorld:
|
|
b.world = g
|
|
default:
|
|
b.user = g
|
|
}
|
|
m.retarget(i, m.seconds(c.Fade, busRamp))
|
|
case CmdBusMute:
|
|
b.mute = c.Flag
|
|
m.retarget(i, m.seconds(0, busRamp))
|
|
default:
|
|
b.solo = c.Flag
|
|
m.resolo()
|
|
}
|
|
case CmdPauseAll:
|
|
m.paused = true
|
|
case CmdResumeAll:
|
|
m.paused = false
|
|
default:
|
|
m.voiceCmd(c)
|
|
}
|
|
}
|
|
|
|
func (m *Mixer) seconds(s, def float32) float64 {
|
|
if s <= 0 {
|
|
s = def
|
|
}
|
|
return float64(s) * m.rate
|
|
}
|
|
|
|
func (m *Mixer) find(id VoiceID) int {
|
|
if int(id.Index) >= len(m.handles) {
|
|
return -1
|
|
}
|
|
h := &m.handles[id.Index]
|
|
if h.gen.Load() != id.Gen {
|
|
return -1
|
|
}
|
|
return int(h.voice)
|
|
}
|
|
|
|
func (m *Mixer) voiceCmd(c *Command) {
|
|
vi := m.find(c.Voice)
|
|
if vi < 0 {
|
|
return
|
|
}
|
|
v := &m.voices[vi]
|
|
switch c.Kind {
|
|
case CmdKill:
|
|
m.end(vi, EvFinished)
|
|
case CmdStop:
|
|
if v.state == voicePaused {
|
|
m.end(vi, EvFinished)
|
|
return
|
|
}
|
|
v.stopping = true
|
|
v.rampTo(0, m.seconds(c.Fade, gainRamp))
|
|
case CmdSetGain:
|
|
if !v.stopping {
|
|
v.rampTo(max(c.Gain, 0), m.seconds(c.Fade, gainRamp))
|
|
}
|
|
case CmdSetPan:
|
|
v.setPan(c.Pan)
|
|
case CmdSetPitch:
|
|
v.setPitch(c.Pitch)
|
|
case CmdPause:
|
|
if v.state == voicePlaying {
|
|
v.state = voicePaused
|
|
}
|
|
case CmdResume:
|
|
if v.state == voicePaused {
|
|
v.state = voicePlaying
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Mixer) playable(c *Command) bool {
|
|
if c.Bus < 0 || int(c.Bus) >= len(m.buses) {
|
|
return false
|
|
}
|
|
switch {
|
|
case c.Memory != nil && c.Stream == nil:
|
|
s := c.Memory
|
|
return (s.Channels == 1 || s.Channels == 2) && s.Rate > 0 && float64(s.Rate) <= MaxRateRatio*m.rate && s.Frames() > 0
|
|
case c.Stream != nil && c.Memory == nil:
|
|
s := c.Stream
|
|
return (s.Channels == 1 || s.Channels == 2) && s.Rate > 0 && float64(s.Rate) <= MaxRateRatio*m.rate && !s.inUse
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *Mixer) play(c *Command) {
|
|
id := c.Voice
|
|
if int(id.Index) >= len(m.handles) {
|
|
return
|
|
}
|
|
h := &m.handles[id.Index]
|
|
if h.gen.Load() != id.Gen || h.voice >= 0 {
|
|
return
|
|
}
|
|
if !m.playable(c) {
|
|
m.reject(id)
|
|
return
|
|
}
|
|
vi := m.take(c.Priority)
|
|
if vi < 0 {
|
|
m.reject(id)
|
|
return
|
|
}
|
|
v := &m.voices[vi]
|
|
*v = voice{id: id, state: voicePlaying, mem: c.Memory, st: c.Stream, bus: int(c.Bus), priority: c.Priority, age: m.ages}
|
|
m.ages++
|
|
if c.Memory != nil {
|
|
v.ch = c.Memory.Channels
|
|
v.ratio = float64(c.Memory.Rate) / m.rate
|
|
v.frames = c.Memory.Frames()
|
|
v.ls, v.le = c.Memory.loopRange()
|
|
v.loop = (c.Flag || c.Memory.Loop) && v.le > v.ls
|
|
} else {
|
|
v.ch = c.Stream.Channels
|
|
v.ratio = float64(c.Stream.Rate) / m.rate
|
|
c.Stream.reset()
|
|
c.Stream.inUse = true
|
|
}
|
|
v.setPitch(c.Pitch)
|
|
v.setPan(c.Pan)
|
|
g := max(c.Gain, 0)
|
|
if c.Fade > 0 {
|
|
v.gain = 0
|
|
v.rampTo(g, float64(c.Fade)*m.rate)
|
|
} else {
|
|
v.gain, v.target = g, g
|
|
}
|
|
h.voice = int32(vi)
|
|
}
|
|
|
|
func (m *Mixer) take(prio int32) int {
|
|
if n := len(m.freeV); n > 0 {
|
|
vi := int(m.freeV[n-1])
|
|
m.freeV = m.freeV[:n-1]
|
|
return vi
|
|
}
|
|
victim := -1
|
|
for i := range m.voices {
|
|
v := &m.voices[i]
|
|
if victim < 0 {
|
|
victim = i
|
|
continue
|
|
}
|
|
w := &m.voices[victim]
|
|
if v.priority < w.priority || (v.priority == w.priority && v.age < w.age) {
|
|
victim = i
|
|
}
|
|
}
|
|
if victim < 0 || m.voices[victim].priority > prio {
|
|
return -1
|
|
}
|
|
m.end(victim, EvStolen)
|
|
n := len(m.freeV)
|
|
vi := int(m.freeV[n-1])
|
|
m.freeV = m.freeV[:n-1]
|
|
return vi
|
|
}
|
|
|
|
func (m *Mixer) release(h uint32) {
|
|
hd := &m.handles[h]
|
|
hd.voice = -1
|
|
hd.pos.Store(0)
|
|
hd.gain.Store(0)
|
|
if hd.gen.Add(1) == 0 {
|
|
hd.gen.Store(1)
|
|
}
|
|
m.hfree.push(h)
|
|
}
|
|
|
|
func (m *Mixer) reject(id VoiceID) {
|
|
m.events.push(Event{Kind: EvRejected, Voice: id})
|
|
m.release(id.Index)
|
|
}
|
|
|
|
func (m *Mixer) end(vi int, kind EventKind) {
|
|
v := &m.voices[vi]
|
|
m.events.push(Event{Kind: kind, Voice: v.id})
|
|
if v.st != nil {
|
|
v.st.inUse = false
|
|
}
|
|
m.release(v.id.Index)
|
|
*v = voice{}
|
|
m.freeV = append(m.freeV, int32(vi))
|
|
}
|
|
|
|
func (m *Mixer) starve(v *voice) {
|
|
if v.starved && m.clock-v.starvedAt < uint64(m.rate) {
|
|
return
|
|
}
|
|
v.starved, v.starvedAt = true, m.clock
|
|
m.events.push(Event{Kind: EvStarved, Voice: v.id})
|
|
}
|
|
|
|
func (m *Mixer) retarget(i int, frames float64) {
|
|
b := &m.buses[i]
|
|
t := b.user * b.session * b.world
|
|
if b.mute || !b.audible {
|
|
t = 0
|
|
}
|
|
b.target = t
|
|
n := int(frames)
|
|
if n < 1 {
|
|
b.gain, b.ramp = t, 0
|
|
return
|
|
}
|
|
b.ramp = n
|
|
b.delta = (t - b.gain) / float32(n)
|
|
}
|
|
|
|
func (m *Mixer) resolo() {
|
|
anySolo := false
|
|
for i := range m.buses {
|
|
anySolo = anySolo || m.buses[i].solo
|
|
}
|
|
for i := range m.buses {
|
|
aud := !anySolo || i == m.master || m.soloLinked(i)
|
|
if aud != m.buses[i].audible {
|
|
m.buses[i].audible = aud
|
|
m.retarget(i, m.seconds(0, busRamp))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Mixer) soloLinked(i int) bool {
|
|
for j := i; j >= 0; j = m.buses[j].parent {
|
|
if m.buses[j].solo {
|
|
return true
|
|
}
|
|
}
|
|
for j := range m.buses {
|
|
if !m.buses[j].solo {
|
|
continue
|
|
}
|
|
for k := m.buses[j].parent; k >= 0; k = m.buses[k].parent {
|
|
if k == i {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *Mixer) publish(v *voice) {
|
|
h := &m.handles[v.id.Index]
|
|
if rate := v.ratio * m.rate; rate > 0 {
|
|
h.pos.Store(math.Float32bits(float32(v.pos / rate)))
|
|
}
|
|
h.gain.Store(math.Float32bits(v.gain))
|
|
}
|
|
|
|
func (m *Mixer) chunk(out []float32, n int) {
|
|
for i := range m.buses {
|
|
if m.buses[i].live {
|
|
clear(m.buses[i].acc[:n*2])
|
|
m.buses[i].live = false
|
|
}
|
|
}
|
|
active := 0
|
|
for vi := range m.voices {
|
|
v := &m.voices[vi]
|
|
if v.state == voiceFree {
|
|
continue
|
|
}
|
|
active++
|
|
if v.state != voicePlaying || m.paused {
|
|
continue
|
|
}
|
|
acc := m.buses[v.bus].acc[:n*2]
|
|
m.buses[v.bus].live = true
|
|
var done bool
|
|
if v.mem != nil {
|
|
if v.step == 1 && v.pos == float64(int64(v.pos)) {
|
|
done = m.renderMemoryDirect(v, acc, n)
|
|
} else {
|
|
done = m.renderMemory(v, acc, n)
|
|
}
|
|
} else {
|
|
done = m.renderStream(v, acc, n)
|
|
}
|
|
if done {
|
|
m.end(vi, EvFinished)
|
|
active--
|
|
continue
|
|
}
|
|
m.publish(v)
|
|
}
|
|
m.active.Store(int32(active))
|
|
m.sum(out, n)
|
|
m.clock += uint64(n)
|
|
}
|
|
|
|
func (m *Mixer) renderMemory(v *voice, acc []float32, n int) bool {
|
|
for f := 0; f < n; f++ {
|
|
if v.stopping && v.ramp == 0 {
|
|
return true
|
|
}
|
|
i := int64(v.pos)
|
|
if !v.loop && i >= v.frames {
|
|
return true
|
|
}
|
|
t := float32(v.pos - float64(i))
|
|
g := v.next()
|
|
l := dsp.Hermite(v.sample(i-1, 0), v.sample(i, 0), v.sample(i+1, 0), v.sample(i+2, 0), t)
|
|
r := l
|
|
if v.ch == 2 {
|
|
r = dsp.Hermite(v.sample(i-1, 1), v.sample(i, 1), v.sample(i+1, 1), v.sample(i+2, 1), t)
|
|
}
|
|
acc[2*f] += l * g * v.gl
|
|
acc[2*f+1] += r * g * v.gr
|
|
v.pos += v.step
|
|
if v.loop && v.pos >= float64(v.le) {
|
|
v.pos -= float64(v.le - v.ls)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// renderMemoryDirect reads a voice at pitch 1 on a whole frame: Hermite at t = 0 is y1, so the output is bit-identical without interpolation; outside a gain ramp it runs a flat loop to the end of the clip or loop.
|
|
func (m *Mixer) renderMemoryDirect(v *voice, acc []float32, n int) bool {
|
|
pcm, ch := v.mem.PCM, v.ch
|
|
i := int64(v.pos)
|
|
f := 0
|
|
for f < n {
|
|
if v.stopping && v.ramp == 0 {
|
|
v.pos = float64(i)
|
|
return true
|
|
}
|
|
end := v.frames
|
|
if v.loop {
|
|
end = v.le
|
|
}
|
|
if i >= end {
|
|
if !v.loop {
|
|
v.pos = float64(i)
|
|
return true
|
|
}
|
|
i = v.ls
|
|
continue
|
|
}
|
|
if v.ramp > 0 {
|
|
g := v.next()
|
|
k := int(i) * ch
|
|
l := pcm[k]
|
|
r := l
|
|
if ch == 2 {
|
|
r = pcm[k+1]
|
|
}
|
|
acc[2*f] += l * g * v.gl
|
|
acc[2*f+1] += r * g * v.gr
|
|
f++
|
|
i++
|
|
continue
|
|
}
|
|
run := int(min(int64(n-f), end-i))
|
|
g, gl, gr := v.gain, v.gl, v.gr
|
|
src := pcm[int(i)*ch : (int(i)+run)*ch]
|
|
dst := acc[2*f : 2*(f+run)]
|
|
if ch == 2 {
|
|
for j := 0; j < run; j++ {
|
|
dst[2*j] += src[2*j] * g * gl
|
|
dst[2*j+1] += src[2*j+1] * g * gr
|
|
}
|
|
} else {
|
|
for j, s := range src {
|
|
dst[2*j] += s * g * gl
|
|
dst[2*j+1] += s * g * gr
|
|
}
|
|
}
|
|
f += run
|
|
i += int64(run)
|
|
}
|
|
v.pos = float64(i)
|
|
return false
|
|
}
|
|
|
|
func (m *Mixer) renderStream(v *voice, acc []float32, n int) bool {
|
|
s := v.st
|
|
eof := s.eof.Load()
|
|
s.fill(v.pos, v.step, n)
|
|
drained := eof && s.Ring.Len() == 0
|
|
end := s.start + int64(s.n)
|
|
for f := 0; f < n; f++ {
|
|
if v.stopping && v.ramp == 0 {
|
|
return true
|
|
}
|
|
i := int64(v.pos)
|
|
if i+2 >= end {
|
|
if !drained {
|
|
m.starve(v)
|
|
return false
|
|
}
|
|
if i >= end {
|
|
return true
|
|
}
|
|
}
|
|
k := int(i - s.start)
|
|
t := float32(v.pos - float64(i))
|
|
g := v.next()
|
|
l := dsp.Hermite(s.at(k-1, 0), s.at(k, 0), s.at(k+1, 0), s.at(k+2, 0), t)
|
|
r := l
|
|
if v.ch == 2 {
|
|
r = dsp.Hermite(s.at(k-1, 1), s.at(k, 1), s.at(k+1, 1), s.at(k+2, 1), t)
|
|
}
|
|
acc[2*f] += l * g * v.gl
|
|
acc[2*f+1] += r * g * v.gr
|
|
v.pos += v.step
|
|
}
|
|
return false
|
|
}
|
|
|
|
func abs32(x float32) float32 {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
return x
|
|
}
|
|
|
|
// idle advances the gain ramp of a bus that got no samples this chunk.
|
|
func (m *Mixer) idle(b *bus, n int) {
|
|
for f := 0; f < n && b.ramp > 0; f++ {
|
|
b.gain += b.delta
|
|
b.ramp--
|
|
if b.ramp == 0 {
|
|
b.gain = b.target
|
|
}
|
|
}
|
|
b.peak.Store(0)
|
|
b.rms.Store(0)
|
|
b.level.Store(math.Float32bits(b.gain))
|
|
}
|
|
|
|
func (m *Mixer) sum(out []float32, n int) {
|
|
for _, bi := range m.order {
|
|
b := &m.buses[bi]
|
|
acc := b.acc[:n*2]
|
|
var dst []float32
|
|
if bi != m.master {
|
|
if !b.live {
|
|
m.idle(b, n)
|
|
continue
|
|
}
|
|
dst = m.buses[b.parent].acc[:n*2]
|
|
m.buses[b.parent].live = true
|
|
}
|
|
var peak, sq float32
|
|
for f := 0; f < n; f++ {
|
|
if b.ramp > 0 {
|
|
b.gain += b.delta
|
|
b.ramp--
|
|
if b.ramp == 0 {
|
|
b.gain = b.target
|
|
}
|
|
}
|
|
l, r := acc[2*f]*b.gain, acc[2*f+1]*b.gain
|
|
if dst == nil {
|
|
l, r = dsp.SoftClip(l), dsp.SoftClip(r)
|
|
out[2*f], out[2*f+1] = l, r
|
|
} else {
|
|
dst[2*f] += l
|
|
dst[2*f+1] += r
|
|
}
|
|
if a := abs32(l); a > peak {
|
|
peak = a
|
|
}
|
|
if a := abs32(r); a > peak {
|
|
peak = a
|
|
}
|
|
sq += l*l + r*r
|
|
}
|
|
b.peak.Store(math.Float32bits(peak))
|
|
b.rms.Store(math.Float32bits(float32(math.Sqrt(float64(sq / float32(2*n))))))
|
|
b.level.Store(math.Float32bits(b.gain))
|
|
}
|
|
}
|