ci / release (push) Successful in 5m5s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 4m41s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m14s
The remaining editor strings and undo step names; component, field, enum, setting, diagnostic and Script API texts under schema.*, enum.*, setting.*, diag.* and api.*; the launcher, the in-game Settings View with a Language setting, the CLI and aego schema show --lang. Every documentation page has a .ru.md translation; the site has a full Russian tree with hreflang, a language switch and ru/llms.txt; aego docs check --lang ru. New doc.tutorial and doc.recipe icons. Fix: an animator restored by a rewind prepares its controller before the replayed step.
180 lines
5.0 KiB
Go
180 lines
5.0 KiB
Go
package editor
|
|
|
|
import (
|
|
"math"
|
|
"strconv"
|
|
|
|
"aego/audio/mix"
|
|
"aego/core/mathx"
|
|
"aego/editor/core/services"
|
|
"aego/editor/core/workspace"
|
|
"aego/engine/i18n"
|
|
"aego/modules/audio"
|
|
"aego/ui/core"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
const (
|
|
stripW = 84
|
|
stripGap = 6
|
|
meterW = 10
|
|
floorDB = -60
|
|
holdFall = 0.6
|
|
buttonW = 120
|
|
)
|
|
|
|
type mixerPanel struct {
|
|
p *plugin
|
|
pct [101]string
|
|
hold [6]float32
|
|
last float64
|
|
stats string
|
|
voices int
|
|
under uint64
|
|
}
|
|
|
|
func newMixer(p *plugin) *mixerPanel {
|
|
m := &mixerPanel{p: p}
|
|
for i := range m.pct {
|
|
m.pct[i] = strconv.Itoa(i) + "%"
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (m *mixerPanel) ID() string {
|
|
return workspace.Mixer
|
|
}
|
|
|
|
func (m *mixerPanel) Title() string {
|
|
return i18n.Tr("i18n.audio.mixer.title", "Audio Mixer")
|
|
}
|
|
|
|
func level(x float32) float32 {
|
|
if x <= 0 {
|
|
return 0
|
|
}
|
|
db := 20 * math.Log10(float64(x))
|
|
return mathx.Clamp(float32((db-floorDB)/-floorDB), 0, 1)
|
|
}
|
|
|
|
func meterColor(lv float32) mathx.Color {
|
|
switch {
|
|
case lv > 0.95:
|
|
return colHigh
|
|
case lv > 0.8:
|
|
return colMid
|
|
}
|
|
return colLow
|
|
}
|
|
|
|
func (m *mixerPanel) Draw(f *services.Frame, r mathx.Rect) {
|
|
ui := f.UI
|
|
mod := m.p.module()
|
|
if mod == nil || mod.Mixer() == nil {
|
|
widgets.LabelDim(ui, i18n.Tr("i18n.audio.the_audio_output_is_not", "The audio output is not open"))
|
|
return
|
|
}
|
|
mx := mod.Mixer()
|
|
var dt float32
|
|
if m.last > 0 {
|
|
dt = float32(f.Now - m.last)
|
|
}
|
|
m.last = f.Now
|
|
m.header(ui, mod)
|
|
pad := ui.Theme.Padding
|
|
top := ui.Cursor().Y
|
|
body := mathx.Rect{X: r.X + pad, Y: top, W: r.W - 2*pad, H: r.Y + r.H - top - pad}
|
|
if body.H < 100 {
|
|
return
|
|
}
|
|
ui.PushClip(body)
|
|
for i := range audio.Buses {
|
|
x := body.X + float32(i)*(stripW+stripGap)
|
|
if i > 0 {
|
|
x += stripGap
|
|
}
|
|
m.strip(ui, mod, mx, i, mathx.Rect{X: x, Y: body.Y, W: stripW, H: body.H}, dt)
|
|
}
|
|
ui.PopClip()
|
|
}
|
|
|
|
func (m *mixerPanel) header(ui *core.Context, mod *audio.Module) {
|
|
st := mod.Stats()
|
|
if m.stats == "" || st.Voices != m.voices || st.Underruns != m.under {
|
|
m.voices, m.under = st.Voices, st.Underruns
|
|
m.stats = i18n.Tr("i18n.audio.mixer_stats", "Voices {voices} Underruns {underruns}", i18n.Int("voices", int64(st.Voices)), i18n.Int("underruns", int64(st.Underruns)))
|
|
}
|
|
row := ui.Cursor().Next(0)
|
|
ui.Text(m.stats, mathx.Vec2{X: row.X, Y: ui.TextY(row)}, ui.Theme.TextDim)
|
|
save := mathx.Rect{X: row.X + row.W - buttonW, Y: row.Y, W: buttonW, H: row.H}
|
|
reset := mathx.Rect{X: save.X - buttonW - 4, Y: row.Y, W: buttonW, H: row.H}
|
|
if widgets.ButtonRect(ui, ui.ID("reset"), reset, i18n.Tr("i18n.audio.reset_session", "Reset Session")) {
|
|
mod.ResetSession()
|
|
}
|
|
if widgets.ButtonRect(ui, ui.ID("save"), save, i18n.Tr("i18n.audio.save_to_profile", "Save to Profile")) {
|
|
if err := mod.SaveSession(); err != nil {
|
|
m.p.notify(err)
|
|
} else {
|
|
m.p.info(i18n.Tr("i18n.audio.bus_volumes_saved", "Bus volumes saved to the user settings"))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *mixerPanel) strip(ui *core.Context, mod *audio.Module, mx *mix.Mixer, i int, r mathx.Rect, dt float32) {
|
|
th := &ui.Theme
|
|
b := audio.Bus(i)
|
|
border := th.Border
|
|
if i == 0 {
|
|
border = th.AccentDim
|
|
}
|
|
ui.Box(r, th.Radius.Button, th.Widget, border)
|
|
rh := th.RowHeight
|
|
name := mathx.Rect{X: r.X, Y: r.Y + 2, W: r.W, H: rh}
|
|
ui.TextIn(name, audio.BusTitle(i), th.Text, core.AlignCenter)
|
|
btn := mathx.Rect{X: r.X + 4, Y: r.Y + r.H - rh - 4, W: (r.W - 12) / 2, H: rh}
|
|
muted := mod.BusMuted(b)
|
|
if widgets.ToggleButton(ui, core.IDSeq(ui.ID("mute"), i), btn, "M", &muted) {
|
|
mod.SetBusMuted(b, muted)
|
|
}
|
|
if i > 0 {
|
|
btn.X += btn.W + 4
|
|
solo := mod.BusSolo(b)
|
|
if widgets.ToggleButton(ui, core.IDSeq(ui.ID("solo"), i), btn, "S", &solo) {
|
|
mod.SetBusSolo(b, solo)
|
|
}
|
|
}
|
|
v := mod.BusVolume(b)
|
|
val := mathx.Rect{X: r.X, Y: btn.Y - rh, W: r.W, H: rh}
|
|
ui.TextIn(val, m.pct[int(mathx.Clamp(v, 0, 1)*100+0.5)], th.TextDim, core.AlignCenter)
|
|
area := mathx.Rect{X: r.X + 8, Y: name.Y + rh + 4, W: r.W - 16, H: val.Y - name.Y - rh - 8}
|
|
if area.H < 20 {
|
|
return
|
|
}
|
|
meter := mathx.Rect{X: area.X, Y: area.Y, W: meterW, H: area.H}
|
|
peak, rms := mx.Meters(i)
|
|
m.hold[i] = max(level(peak), m.hold[i]-dt*holdFall)
|
|
ui.Rect(meter, th.Background)
|
|
lv := level(rms)
|
|
ui.Rect(mathx.Rect{X: meter.X, Y: meter.Y + meter.H*(1-lv), W: meter.W, H: meter.H * lv}, meterColor(lv))
|
|
ui.Rect(mathx.Rect{X: meter.X, Y: meter.Y + meter.H*(1-m.hold[i]), W: meter.W, H: 2}, meterColor(m.hold[i]))
|
|
ui.Outline(meter, th.Border)
|
|
track := mathx.Rect{X: meter.X + meter.W + 12, Y: area.Y, W: area.W - meter.W - 12, H: area.H}
|
|
m.fader(ui, mod, b, i, track, v)
|
|
}
|
|
|
|
func (m *mixerPanel) fader(ui *core.Context, mod *audio.Module, b audio.Bus, i int, r mathx.Rect, v float32) {
|
|
th := &ui.Theme
|
|
id := core.IDSeq(ui.ID("fader"), i)
|
|
ui.RoundRect(mathx.Rect{X: r.X + r.W/2 - 2, Y: r.Y, W: 4, H: r.H}, 2, th.Background)
|
|
if d, ok := ui.Drag(id, r); ok && d.Y != 0 && r.H > 0 {
|
|
v = mathx.Clamp(v-d.Y/r.H, 0, 1)
|
|
mod.SetBusVolume(b, v)
|
|
}
|
|
if ui.DoubleClicked(id) {
|
|
v = 1
|
|
mod.SetBusVolume(b, v)
|
|
}
|
|
ky := r.Y + r.H*(1-v)
|
|
ui.Box(mathx.Rect{X: r.X, Y: ky - 5, W: r.W, H: 10}, th.Radius.Button, ui.WidgetColor(id), th.Border)
|
|
}
|