- mixer.Init: convert database 'default' to '' for SDL default device - AudioOptions.Init: fallback to first SDL device when current device is default and config stores 'default' Fixes known issue from changelog.txt
627 lines
20 KiB
Go
627 lines
20 KiB
Go
package page
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
|
|
"github.com/go-gl/gl/v3.3-core/gl"
|
|
"github.com/go-gl/mathgl/mgl32"
|
|
"github.com/veandco/go-sdl2/sdl"
|
|
"rhemawavetech.com/rhemawave/internal/colors"
|
|
"rhemawavetech.com/rhemawave/internal/db"
|
|
"rhemawavetech.com/rhemawave/internal/fontManager"
|
|
"rhemawavetech.com/rhemawave/internal/logger"
|
|
"rhemawavetech.com/rhemawave/internal/mixer"
|
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
|
)
|
|
|
|
// focusToggle is the sentinel focusState value meaning the audio on/off toggle
|
|
// row has keyboard focus. Device indices (0..n-1) are used for everything below.
|
|
const focusToggle = -1
|
|
|
|
// AudioFileList is kept as the canonical seed data for the audio_files table.
|
|
// Track selection UI no longer lives on this page, but other parts of the
|
|
// app still read these rows from the database.
|
|
var AudioFileList = []db.AudioFile{
|
|
{ID: 14, Name: "Original Audio", Path: "OriginalAudio.wav", Selected: false, Order: 1},
|
|
{ID: 1, Name: "174 Hz", Path: "174Hz.wav", Selected: false, Order: 2},
|
|
{ID: 2, Name: "396 Hz", Path: "396Hz.wav", Selected: false, Order: 3},
|
|
{ID: 3, Name: "417 Hz", Path: "417Hz.wav", Selected: false, Order: 4},
|
|
{ID: 4, Name: "432 Hz", Path: "432Hz.wav", Selected: false, Order: 5},
|
|
{ID: 5, Name: "639 Hz", Path: "639Hz.wav", Selected: false, Order: 6},
|
|
{ID: 6, Name: "741 Hz", Path: "741Hz.wav", Selected: false, Order: 7},
|
|
{ID: 7, Name: "852 Hz", Path: "852Hz.wav", Selected: false, Order: 8},
|
|
{ID: 8, Name: "963 Hz", Path: "963Hz.wav", Selected: false, Order: 9},
|
|
{ID: 9, Name: "1111 Hz", Path: "1111Hz.wav", Selected: false, Order: 10},
|
|
{ID: 10, Name: "14 Hz Binaural Beat", Path: "14Hz_Binaural.wav", Selected: false, Order: 11},
|
|
{ID: 11, Name: "40 Hz Binaural Beat", Path: "40Hz_Binaural.wav", Selected: false, Order: 12},
|
|
{ID: 12, Name: "Alpha Waves", Path: "AlphaWaves.wav", Selected: false, Order: 13},
|
|
{ID: 13, Name: "Pineal Gland", Path: "PinealGland.wav", Selected: false, Order: 14},
|
|
}
|
|
|
|
const triangleSize = float32(16)
|
|
|
|
type AudioDevice struct {
|
|
Label string
|
|
ID uint32
|
|
index int
|
|
}
|
|
|
|
type AudioOptions struct {
|
|
// Page Attributes
|
|
Base
|
|
font *fontManager.FontEntry
|
|
textShader *shaderManager.Shader
|
|
solidShader *shaderManager.Shader
|
|
CfgPtr *db.AppConfig
|
|
|
|
// Page Textures
|
|
title StringItem
|
|
prompt StringItem
|
|
|
|
// Layout
|
|
verticalMargin int32
|
|
ItemScale float32
|
|
|
|
// Audio toggle
|
|
AudioStatusToggle StringItem
|
|
|
|
// Audio Devices
|
|
activeDeviceIndex int
|
|
NumAudioDevices int
|
|
AudioDevices []AudioDevice
|
|
AudioDeviceTextures []StringItem
|
|
|
|
// Volume row
|
|
VolumeLabel StringItem
|
|
volumePercent StringItem
|
|
volumeBarX int32
|
|
volumeBarY int32
|
|
volumeBarW int32
|
|
volumeBarH int32
|
|
|
|
// Triangle cursor
|
|
triVAO uint32
|
|
triVBO uint32
|
|
triX float32
|
|
triCenterY float32
|
|
spinElapsed float32
|
|
|
|
// Focus: focusToggle (-1) = toggle row, 0..n-1 = device at that index
|
|
focusState int
|
|
}
|
|
|
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
|
|
|
func (p *AudioOptions) Init(ctx *PageCtx) error {
|
|
var err error
|
|
p.verticalMargin = int32(20)
|
|
p.focusState = focusToggle
|
|
|
|
// Font
|
|
fm := fontManager.Get()
|
|
font, ok := fm.GetFont("SpaceMonoNerdFont-Regular")
|
|
if !ok {
|
|
return fmt.Errorf("failed to get font: %w", sdl.GetError())
|
|
}
|
|
p.font = font
|
|
|
|
// Shaders
|
|
sm := shaderManager.Get()
|
|
p.textShader, ok = sm.Get("text")
|
|
if !ok {
|
|
return fmt.Errorf("failed to get text shader: %w", sdl.GetError())
|
|
}
|
|
p.solidShader, ok = sm.Get("solid_color")
|
|
if !ok {
|
|
return fmt.Errorf("failed to get solid_color shader: %w", sdl.GetError())
|
|
}
|
|
|
|
if err = p.Base.Init(ctx); err != nil {
|
|
return fmt.Errorf("base init failed: %w", err)
|
|
}
|
|
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
baseFontSize := config.StandardFontSize
|
|
titleScale := float32(64) / float32(baseFontSize)
|
|
p.ItemScale = float32(32) / float32(baseFontSize)
|
|
|
|
// Title
|
|
p.title, err = NewStringItem("Audio Options", p.font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("creating title: %w", err)
|
|
}
|
|
p.title.Scale(titleScale)
|
|
|
|
// Prompt
|
|
p.prompt, err = NewStringItem(
|
|
"Enter to select -- Up/Down to navigate -- Left/Right to change volume -- Backspace to go back",
|
|
p.font, colors.White,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("creating prompt: %w", err)
|
|
}
|
|
p.prompt.Scale(p.ItemScale)
|
|
|
|
// Volume row
|
|
p.VolumeLabel, err = NewStringItem("Volume", p.font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("creating volume label: %w", err)
|
|
}
|
|
p.VolumeLabel.Scale(p.ItemScale)
|
|
if err = p.CreateVolumePercentItem(); err != nil {
|
|
logger.Error("failed to create VolumePercentItem in AudioOptions.Init()")
|
|
}
|
|
|
|
// Audio toggle
|
|
if err = p.CreateAudioStatusItem(); err != nil {
|
|
logger.Error("failed to create AudioStatusItem in AudioOptions.Init()")
|
|
}
|
|
|
|
// Audio output devices
|
|
if err = p.GetAudioDevices(); err != nil {
|
|
logger.Error("failed to enumerate audio devices: %v", err)
|
|
}
|
|
|
|
// Set active device index based on mixer's current device
|
|
currentName := mixer.CurrentDevice()
|
|
config, err = db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
p.activeDeviceIndex = -1 // default to not found
|
|
for i, dev := range p.AudioDevices {
|
|
if dev.Label == currentName {
|
|
p.activeDeviceIndex = i
|
|
break
|
|
}
|
|
}
|
|
// If not found and current device is default (""), check saved config
|
|
if p.activeDeviceIndex == -1 && currentName == "" && config.SelectedAudioDevice == "default" {
|
|
// Default device is selected - first device in SDL list is typically the default
|
|
if len(p.AudioDevices) > 0 {
|
|
p.activeDeviceIndex = 0
|
|
}
|
|
}
|
|
|
|
// Make sure the audio_files table is populated.
|
|
if err = p.seedAudioFiles(); err != nil {
|
|
logger.Error("failed to seed audio files: %v", err)
|
|
}
|
|
|
|
// Triangle cursor VAO/VBO — stride is vec2 only, no tex coords
|
|
triVerts := []float32{
|
|
0.5, 0.0,
|
|
-0.5, 0.5,
|
|
-0.5, -0.5,
|
|
}
|
|
gl.GenVertexArrays(1, &p.triVAO)
|
|
gl.GenBuffers(1, &p.triVBO)
|
|
gl.BindVertexArray(p.triVAO)
|
|
gl.BindBuffer(gl.ARRAY_BUFFER, p.triVBO)
|
|
gl.BufferData(gl.ARRAY_BUFFER, len(triVerts)*4, gl.Ptr(triVerts), gl.STATIC_DRAW)
|
|
gl.EnableVertexAttribArray(0)
|
|
gl.VertexAttribPointerWithOffset(0, 2, gl.FLOAT, false, 2*4, 0)
|
|
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
|
gl.BindVertexArray(0)
|
|
|
|
p.layout()
|
|
return nil
|
|
}
|
|
|
|
// seedAudioFiles ensures the audio_files table has its default rows.
|
|
func (p *AudioOptions) seedAudioFiles() error {
|
|
var existing []db.AudioFile
|
|
if err := p.Context.DbPtr.Find(&existing).Error; err != nil {
|
|
return err
|
|
}
|
|
if len(existing) == 0 {
|
|
return p.Context.DbPtr.Save(&AudioFileList).Error
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ── Audio status toggle ───────────────────────────────────────────────────────
|
|
|
|
func (p *AudioOptions) CreateAudioStatusItem() error {
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
on := config.AudioEnabled
|
|
statusString := "Turn Audio On"
|
|
color := colors.Green
|
|
if on {
|
|
statusString = "Turn Audio Off"
|
|
color = colors.Red
|
|
}
|
|
item, err := NewStringItem(statusString, p.font, color)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create '%s' string item: %v", statusString, err)
|
|
}
|
|
item.Scale(p.ItemScale)
|
|
p.AudioStatusToggle = item
|
|
return nil
|
|
}
|
|
|
|
// ── Audio Output Devices ──────────────────────────────────────────────────────
|
|
|
|
func (p *AudioOptions) GetAudioDevices() error {
|
|
numAudioDevices := sdl.GetNumAudioDevices(false)
|
|
if numAudioDevices == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Pre-allocate with zero length so append doesn't produce leading zero-value
|
|
// elements (the old code used make([]AudioDevice, n) then appended, which
|
|
// doubled the slice length with n zero-value entries at the front).
|
|
audioDevices := make([]AudioDevice, 0, numAudioDevices)
|
|
for i := range numAudioDevices {
|
|
label := sdl.GetAudioDeviceName(i, false)
|
|
audioDevices = append(audioDevices, AudioDevice{Label: label, ID: uint32(i)})
|
|
}
|
|
p.AudioDevices = audioDevices
|
|
|
|
for i := range audioDevices {
|
|
item, err := NewStringItem(p.AudioDevices[i].Label, p.font, colors.White)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
item.Scale(p.ItemScale)
|
|
p.AudioDeviceTextures = append(p.AudioDeviceTextures, item)
|
|
}
|
|
p.NumAudioDevices = numAudioDevices
|
|
return nil
|
|
}
|
|
|
|
// SelectAudioDevice is called when the user confirms a device from the list.
|
|
// The device-switching implementation goes here; for now we just log the choice.
|
|
func (p *AudioOptions) SelectAudioDevice(deviceIndex int) error {
|
|
if deviceIndex < 0 || deviceIndex >= len(p.AudioDevices) {
|
|
return fmt.Errorf("invalid device index: %d", deviceIndex)
|
|
}
|
|
device := p.AudioDevices[deviceIndex]
|
|
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get config for device switch: %w", err)
|
|
}
|
|
|
|
if err := mixer.SwitchDevice(device.Label, config); err != nil {
|
|
return fmt.Errorf("failed to switch to device %s: %w", device.Label, err)
|
|
}
|
|
|
|
// Persist the selected device to the database
|
|
config.SelectedAudioDevice = device.Label
|
|
if err := p.Context.DbPtr.Save(config).Error; err != nil {
|
|
logger.Error("failed to persist device selection: %v", err)
|
|
}
|
|
|
|
p.activeDeviceIndex = deviceIndex
|
|
return nil
|
|
}
|
|
|
|
// ── Volume ────────────────────────────────────────────────────────────────────
|
|
|
|
func (p *AudioOptions) CreateVolumePercentItem() error {
|
|
if p.volumePercent.ID != 0 {
|
|
gl.DeleteTextures(1, &p.volumePercent.ID)
|
|
}
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
pct := int(config.SavedVolume * 100)
|
|
item, err := NewStringItem(fmt.Sprintf("%d%%", pct), p.font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create volume percent item: %v", err)
|
|
}
|
|
item.Scale(p.ItemScale)
|
|
p.volumePercent = item
|
|
return nil
|
|
}
|
|
|
|
func (p *AudioOptions) UpdateVolume(level int) {
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
newVol := mixer.ConvertVolume(level) + config.SavedVolume
|
|
if newVol < 0.0 || newVol > 1.0 {
|
|
return
|
|
}
|
|
if newVol > .99 {
|
|
newVol = 1.0
|
|
}
|
|
config.SavedVolume = newVol
|
|
p.Context.DbPtr.Save(&config)
|
|
if err := mixer.SetAllSinkVolumes(newVol); err != nil {
|
|
logger.Error("Error setting allSinkVolumes: %v", err)
|
|
}
|
|
if err := p.CreateVolumePercentItem(); err != nil {
|
|
logger.Error("failed to refresh volume percent item: %v", err)
|
|
}
|
|
gap := int32(12)
|
|
p.volumePercent.X = p.volumeBarX + p.volumeBarW + gap
|
|
p.volumePercent.Y = p.volumeBarY + (p.volumeBarH-p.volumePercent.H)/2
|
|
}
|
|
|
|
// ── Layout ────────────────────────────────────────────────────────────────────
|
|
|
|
func (p *AudioOptions) layout() {
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Title
|
|
p.title.X = p.ScreenCenterX - p.title.W/2
|
|
p.title.Y = p.ScreenHeight - p.title.H - p.verticalMargin*2
|
|
|
|
// Prompt
|
|
p.prompt.X = p.ScreenCenterX - p.prompt.W/2
|
|
p.prompt.Y = p.title.Y - p.verticalMargin - p.prompt.H
|
|
|
|
// Volume row
|
|
barH := int32(float32(config.StandardFontSize) * p.ItemScale)
|
|
barW := int32(float32(p.ScreenWidth) * 0.30)
|
|
barX := p.ScreenCenterX - barW/2
|
|
barY := p.prompt.Y - p.verticalMargin*2 - barH
|
|
|
|
p.volumeBarX = barX
|
|
p.volumeBarY = barY
|
|
p.volumeBarW = barW
|
|
p.volumeBarH = barH
|
|
|
|
p.VolumeLabel.X = barX - p.VolumeLabel.W - 20
|
|
p.VolumeLabel.Y = barY + (barH-p.VolumeLabel.H)/2
|
|
|
|
gap := int32(12)
|
|
p.volumePercent.X = barX + barW + gap
|
|
p.volumePercent.Y = barY + (barH-p.volumePercent.H)/2
|
|
|
|
// Audio toggle — centred below the volume row
|
|
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
|
|
p.AudioStatusToggle.Y = barY - p.verticalMargin*3 - p.AudioStatusToggle.H
|
|
|
|
// Device list — each item centred and stacked below the toggle
|
|
nextY := p.AudioStatusToggle.Y - p.verticalMargin
|
|
for i := range p.AudioDeviceTextures {
|
|
item := &p.AudioDeviceTextures[i]
|
|
item.X = p.ScreenCenterX - item.W/2
|
|
item.Y = nextY - item.H
|
|
nextY = item.Y - p.verticalMargin
|
|
}
|
|
|
|
// Reposition the triangle cursor for whatever row currently has focus
|
|
p.updateCursorPosition()
|
|
}
|
|
|
|
// updateCursorPosition moves triX / triCenterY to sit just left of the
|
|
// focused row. Call this any time focusState changes or layout() reruns.
|
|
func (p *AudioOptions) updateCursorPosition() {
|
|
cursorGap := int32(12)
|
|
|
|
var item *StringItem
|
|
switch {
|
|
case p.focusState == focusToggle:
|
|
item = &p.AudioStatusToggle
|
|
case p.focusState >= 0 && p.focusState < len(p.AudioDeviceTextures):
|
|
item = &p.AudioDeviceTextures[p.focusState]
|
|
default:
|
|
return
|
|
}
|
|
|
|
p.triX = float32(item.X) - triangleSize - float32(cursorGap)
|
|
p.triCenterY = float32(item.Y) + float32(item.H)/2
|
|
}
|
|
|
|
// ── Toggle ────────────────────────────────────────────────────────────────────
|
|
|
|
func (p *AudioOptions) ToggleAudio() {
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
logger.Error("panic! Cannot query database for configuration")
|
|
panic(err)
|
|
}
|
|
config.AudioEnabled = !config.AudioEnabled
|
|
p.Context.DbPtr.Save(config)
|
|
p.CreateAudioStatusItem()
|
|
p.layout()
|
|
}
|
|
|
|
// ── Events ────────────────────────────────────────────────────────────────────
|
|
|
|
func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
|
|
e, ok := (*event).(*sdl.KeyboardEvent)
|
|
if !ok || e.Type != sdl.KEYDOWN {
|
|
return nil
|
|
}
|
|
|
|
switch e.Keysym.Sym {
|
|
case sdl.K_RETURN:
|
|
if p.focusState == focusToggle {
|
|
p.ToggleAudio()
|
|
} else if p.focusState >= 0 && p.focusState < len(p.AudioDevices) {
|
|
if err := p.SelectAudioDevice(p.focusState); err != nil {
|
|
logger.Error("SelectAudioDevice: %v", err)
|
|
}
|
|
}
|
|
|
|
case sdl.K_DOWN:
|
|
if p.focusState == focusToggle {
|
|
if len(p.AudioDeviceTextures) > 0 {
|
|
p.focusState = 0
|
|
p.updateCursorPosition()
|
|
}
|
|
} else if p.focusState < len(p.AudioDeviceTextures)-1 {
|
|
p.focusState++
|
|
p.updateCursorPosition()
|
|
}
|
|
|
|
case sdl.K_UP:
|
|
if p.focusState == 0 {
|
|
p.focusState = focusToggle
|
|
p.updateCursorPosition()
|
|
} else if p.focusState > 0 {
|
|
p.focusState--
|
|
p.updateCursorPosition()
|
|
}
|
|
|
|
case sdl.K_RIGHT:
|
|
p.UpdateVolume(10)
|
|
|
|
case sdl.K_LEFT:
|
|
p.UpdateVolume(-10)
|
|
|
|
case sdl.K_BACKSPACE:
|
|
p.Context.NavigateTo(SETTINGS)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ── Update ────────────────────────────────────────────────────────────────────
|
|
|
|
func (p *AudioOptions) Update(dt float32) error {
|
|
p.spinElapsed += dt
|
|
if p.spinElapsed >= 1.0 {
|
|
p.spinElapsed -= 1.0
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ── Render helpers ────────────────────────────────────────────────────────────
|
|
|
|
func (p *AudioOptions) renderVolumeBar() error {
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
vol := config.SavedVolume
|
|
|
|
fillW := float32(p.volumeBarW) * float32(vol)
|
|
if fillW > 0 {
|
|
if err := p.RenderSolidColorQuad(
|
|
p.solidShader,
|
|
mgl32.Vec2{float32(p.volumeBarX), float32(p.volumeBarY)},
|
|
mgl32.Vec2{fillW, float32(p.volumeBarH)},
|
|
colors.White,
|
|
); err != nil {
|
|
return fmt.Errorf("rendering volume fill: %w", err)
|
|
}
|
|
}
|
|
|
|
thickness := float32(2)
|
|
bx := float32(p.volumeBarX)
|
|
by := float32(p.volumeBarY)
|
|
bw := float32(p.volumeBarW)
|
|
bh := float32(p.volumeBarH)
|
|
|
|
outlineRects := [4][2]mgl32.Vec2{
|
|
{{bx, by}, {bw, thickness}},
|
|
{{bx, by + bh - thickness}, {bw, thickness}},
|
|
{{bx, by}, {thickness, bh}},
|
|
{{bx + bw - thickness, by}, {thickness, bh}}, // fix: was missing `by` as Y component
|
|
}
|
|
for _, r := range outlineRects {
|
|
if err := p.RenderSolidColorQuad(p.solidShader, r[0], r[1], colors.Green); err != nil {
|
|
return fmt.Errorf("rendering volume outline: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// renderCursor draws the spinning triangle cursor next to the focused row.
|
|
func (p *AudioOptions) renderCursor() error {
|
|
scaleY := float32(math.Cos(2 * math.Pi * float64(p.spinElapsed)))
|
|
|
|
model := mgl32.Ident4()
|
|
model = model.Mul4(mgl32.Translate3D(p.triX+triangleSize/2, p.triCenterY, 0))
|
|
model = model.Mul4(mgl32.Scale3D(triangleSize, triangleSize*scaleY, 1))
|
|
mvp := p.Projection.Mul4(model)
|
|
|
|
gl.UseProgram(p.solidShader.ProgramID)
|
|
gl.UniformMatrix4fv(p.solidShader.LocMVPMatrix, 1, false, &mvp[0])
|
|
gl.Uniform4f(
|
|
p.solidShader.LocColor,
|
|
float32(colors.White.R)/255.0,
|
|
float32(colors.White.G)/255.0,
|
|
float32(colors.White.B)/255.0,
|
|
float32(colors.White.A)/255.0,
|
|
)
|
|
|
|
gl.BindVertexArray(p.triVAO)
|
|
gl.DrawArrays(gl.TRIANGLES, 0, 3)
|
|
gl.BindVertexArray(0)
|
|
gl.UseProgram(0)
|
|
|
|
return nil
|
|
}
|
|
|
|
// ── Render ────────────────────────────────────────────────────────────────────
|
|
|
|
func (p *AudioOptions) Render() error {
|
|
if err := p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color); err != nil {
|
|
return fmt.Errorf("render title: %w", err)
|
|
}
|
|
if err := p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color); err != nil {
|
|
return fmt.Errorf("render prompt: %w", err)
|
|
}
|
|
|
|
// Volume row
|
|
if err := p.RenderTexture(p.textShader, p.VolumeLabel.ID, p.VolumeLabel.Position(), p.VolumeLabel.RawDimensions(), p.VolumeLabel.Color); err != nil {
|
|
return fmt.Errorf("render volume label: %w", err)
|
|
}
|
|
if err := p.renderVolumeBar(); err != nil {
|
|
return fmt.Errorf("render volume bar: %w", err)
|
|
}
|
|
if err := p.RenderTexture(p.textShader, p.volumePercent.ID, p.volumePercent.Position(), p.volumePercent.RawDimensions(), p.volumePercent.Color); err != nil {
|
|
return fmt.Errorf("render volume percent: %w", err)
|
|
}
|
|
|
|
// Toggle
|
|
if err := p.RenderTexture(p.textShader, p.AudioStatusToggle.ID, p.AudioStatusToggle.Position(), p.AudioStatusToggle.RawDimensions(), p.AudioStatusToggle.Color); err != nil {
|
|
return fmt.Errorf("render audio toggle: %w", err)
|
|
}
|
|
|
|
// Device list
|
|
for i := range p.AudioDeviceTextures {
|
|
item := &p.AudioDeviceTextures[i]
|
|
color := colors.White
|
|
if i == p.activeDeviceIndex {
|
|
color = colors.Green
|
|
}
|
|
if err := p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), color); err != nil {
|
|
return fmt.Errorf("render audio device %d: %w", i, err)
|
|
}
|
|
}
|
|
|
|
// Spinning triangle cursor — always on top
|
|
if err := p.renderCursor(); err != nil {
|
|
return fmt.Errorf("render cursor: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ── Destroy ───────────────────────────────────────────────────────────────────
|
|
|
|
func (p *AudioOptions) Destroy() error {
|
|
gl.DeleteTextures(1, &p.title.ID)
|
|
gl.DeleteTextures(1, &p.prompt.ID)
|
|
gl.DeleteTextures(1, &p.VolumeLabel.ID)
|
|
gl.DeleteTextures(1, &p.volumePercent.ID)
|
|
gl.DeleteTextures(1, &p.AudioStatusToggle.ID)
|
|
for i := range p.AudioDeviceTextures {
|
|
gl.DeleteTextures(1, &p.AudioDeviceTextures[i].ID)
|
|
}
|
|
gl.DeleteBuffers(1, &p.triVBO)
|
|
gl.DeleteVertexArrays(1, &p.triVAO)
|
|
return nil
|
|
}
|