Files
radiantwave-legacy/internal/page/AudioOptions.go
T
NeilClack 91711eef93 Replace AudioFile with AudioTrack, fix download+orphan+MD5 sync, always play original audio fallback
AudioTrack table replaces AudioFile (Filename, Name, AssignedFrequency,
MD5, Selected, Order) — fixes relative-path bug in scrollerPage.go that
caused "file not found" for single-track selection.

Download goroutine now:
  - Accepts *gorm.DB and updates AudioTrack.MD5 after each download.
  - Treats checksums.md5 as source of truth: removes local files and DB
    records not in the manifest (orphans).
  - Strips "./" prefix from manifest filenames (GNU md5sum format).

AudioOptions.go seeds 14 AudioTrack records matching server checksums.md5.
List background quad width matches the widest item (+ padding), centered.

scrollerPage.go switchAudioForFrequency falls back to original/selected
audio when a frequency has no matching track or file.  playOriginalAudio
is a no-op on repeat calls to avoid 5-second audio restart loop.

Fixes progress display: passes Max=0 during downloads so Home page shows
"x / X Files Downloaded" text instead of bogus "100.0%" percentage.

Downstream audio switching (DynamicAudio) verifies an AudioTrack exists
for the frequency before assigning it to a stream — frequencies without
a track (e.g. 528 Hz) gracefully use original audio instead.
2026-07-29 16:19:15 -05:00

737 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"
)
const focusToggle = -1
const triangleSize = float32(16)
var audioTrackSeed = []db.AudioTrack{
{Filename: "OriginalAudio.wav", Name: "Original Audio", AssignedFrequency: 0, MD5: "d39c2676c315438752ab0c55e9427d74", Order: 1},
{Filename: "174Hz.wav", Name: "174 Hz", AssignedFrequency: 174, MD5: "42abaf332e628b646ac2cc9d1b5385e7", Order: 2},
{Filename: "396Hz.wav", Name: "396 Hz", AssignedFrequency: 396, MD5: "d53d45849b042122a3c177fad95ff499", Order: 3},
{Filename: "417Hz.wav", Name: "417 Hz", AssignedFrequency: 417, MD5: "056a7aae0b53111f5c568ea7ecd1fadb", Order: 4},
{Filename: "432Hz.wav", Name: "432 Hz", AssignedFrequency: 432, MD5: "276c57179372404ff59c35024911468e", Order: 5},
{Filename: "639Hz.wav", Name: "639 Hz", AssignedFrequency: 639, MD5: "4e460148230f4417fef39e9817f75e06", Order: 6},
{Filename: "741Hz.wav", Name: "741 Hz", AssignedFrequency: 741, MD5: "cfbd9b03b539db69bcefdd91cedc9ad1", Order: 7},
{Filename: "852Hz.wav", Name: "852 Hz", AssignedFrequency: 852, MD5: "485700296d017f8a1b0a7583a55a7783", Order: 8},
{Filename: "963Hz.wav", Name: "963 Hz", AssignedFrequency: 963, MD5: "2670e13294daef8490a862c078aa8af4", Order: 9},
{Filename: "1111Hz.wav", Name: "1111 Hz", AssignedFrequency: 1111, MD5: "67bbc7619dc21d0750519113472ca335", Order: 10},
{Filename: "14Hz_Binaural.wav", Name: "14 Hz Binaural Beat", AssignedFrequency: 14, MD5: "807c14866640d2ab27a6f5d7fdd24e77", Order: 11},
{Filename: "40Hz_Binaural.wav", Name: "40 Hz Binaural Beat", AssignedFrequency: 40, MD5: "42d60d55a16db4e9b29da47a5a102f2c", Order: 12},
{Filename: "AlphaWaves.wav", Name: "Alpha Waves", AssignedFrequency: 0, MD5: "07a93fa0e633a00d97b8a5e5a259d6ff", Order: 13},
{Filename: "PinealGland.wav", Name: "Pineal Gland", AssignedFrequency: 0, MD5: "998d74e3826b30086d64ff0df7b9f133", Order: 14},
}
type AudioOptions struct {
Base
font *fontManager.FontEntry
textShader *shaderManager.Shader
solidShader *shaderManager.Shader
title StringItem
prompt StringItem
verticalMargin int32
ItemScale float32
AudioStatusToggle StringItem
audioFileTextures []StringItem
audioTracks []db.AudioTrack
selectedFileIndex int
VolumeLabel StringItem
volumePercent StringItem
volumeBarX int32
volumeBarY int32
volumeBarW int32
volumeBarH int32
triVAO uint32
triVBO uint32
triX float32
triCenterY float32
spinElapsed float32
focusState int
sectionHeader StringItem
scrollOffset int
maxVisible int
listItemMargin int32
listAreaTop int32
listAreaBottom int32
listItemHeight int32
scrollTrackWidth int32
scrollTrackX int32
scrollTrackY int32
scrollTrackH int32
dotSize float32
listLeftX int32
}
func (p *AudioOptions) Init(ctx *PageCtx) error {
var err error
p.verticalMargin = int32(20)
p.listItemMargin = 6
p.focusState = focusToggle
p.scrollOffset = 0
p.dotSize = 8.0
p.scrollTrackWidth = 30
p.scrollTrackX = p.ScreenWidth - p.scrollTrackWidth
fm := fontManager.Get()
font, ok := fm.GetFont("SpaceMonoNerdFont-Regular")
if !ok {
return fmt.Errorf("failed to get font: %w", sdl.GetError())
}
p.font = font
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)
p.title, err = NewStringItem("Audio Options", p.font, colors.White)
if err != nil {
return fmt.Errorf("creating title: %w", err)
}
p.title.Scale(titleScale)
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)
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()")
}
if err = p.CreateAudioStatusItem(); err != nil {
logger.Error("failed to create AudioStatusItem in AudioOptions.Init()")
}
if err = p.seedAudioTracks(); err != nil {
logger.Error("failed to seed audio tracks: %v", err)
}
p.buildAudioFileList()
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.sectionHeader, err = NewStringItem("Audio Track", p.font, colors.Silver)
if err != nil {
return fmt.Errorf("creating section header: %w", err)
}
p.sectionHeader.Scale(p.ItemScale * 1.2)
p.layout()
return nil
}
func (p *AudioOptions) buildAudioFileList() {
for i := range p.audioFileTextures {
gl.DeleteTextures(1, &p.audioFileTextures[i].ID)
}
p.audioFileTextures = nil
var tracks []db.AudioTrack
p.Context.DbPtr.Order("`order` ASC").Find(&tracks)
p.audioTracks = tracks
dynamicItem, err := NewStringItem("Dynamic Audio", p.font, colors.White)
if err != nil {
logger.Error("failed to create Dynamic Audio string item: %v", err)
} else {
dynamicItem.Scale(p.ItemScale)
p.audioFileTextures = append(p.audioFileTextures, dynamicItem)
}
for _, track := range tracks {
item, err := NewStringItem(track.Name, p.font, colors.White)
if err != nil {
logger.Error("failed to create audio track string item for %s: %v", track.Name, err)
continue
}
item.Scale(p.ItemScale)
p.audioFileTextures = append(p.audioFileTextures, item)
}
p.updateSelectedIndex()
}
func (p *AudioOptions) updateSelectedIndex() {
config, err := db.GetConfig(p.Context.DbPtr)
if err != nil {
p.selectedFileIndex = 0
return
}
if config.DynamicAudio {
p.selectedFileIndex = 0
return
}
var selected db.AudioTrack
if err := p.Context.DbPtr.Where("selected = ?", true).First(&selected).Error; err != nil {
p.selectedFileIndex = 0
return
}
for i, track := range p.audioTracks {
if track.ID == selected.ID {
p.selectedFileIndex = i + 1
return
}
}
p.selectedFileIndex = 0
}
func (p *AudioOptions) seedAudioTracks() error {
var existing []db.AudioTrack
if err := p.Context.DbPtr.Find(&existing).Error; err != nil {
return err
}
if len(existing) == 0 {
return p.Context.DbPtr.Save(&audioTrackSeed).Error
}
return nil
}
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
}
func (p *AudioOptions) selectAudioFile(index int) {
config, err := db.GetConfig(p.Context.DbPtr)
if err != nil {
panic(err)
}
if index == 0 {
config.DynamicAudio = true
p.Context.DbPtr.Model(&db.AudioTrack{}).Where("1 = 1").Update("selected", false)
} else {
config.DynamicAudio = false
trackIndex := index - 1
if trackIndex >= 0 && trackIndex < len(p.audioTracks) {
p.Context.DbPtr.Model(&db.AudioTrack{}).Where("1 = 1").Update("selected", false)
p.Context.DbPtr.Model(&db.AudioTrack{}).Where("id = ?", p.audioTracks[trackIndex].ID).Update("selected", true)
}
}
if err := p.Context.DbPtr.Save(config).Error; err != nil {
logger.Error("failed to save audio config: %v", err)
}
p.selectedFileIndex = index
}
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.SetVolume(newVol); err != nil {
logger.Error("Error setting volume: %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
}
func (p *AudioOptions) layout() {
config, err := db.GetConfig(p.Context.DbPtr)
if err != nil {
panic(err)
}
p.title.X = p.ScreenCenterX - p.title.W/2
p.title.Y = p.ScreenHeight - p.title.H - p.verticalMargin*2
p.prompt.X = p.ScreenCenterX - p.prompt.W/2
p.prompt.Y = p.title.Y - p.verticalMargin - p.prompt.H
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
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
p.AudioStatusToggle.Y = barY - p.verticalMargin*3 - p.AudioStatusToggle.H
p.sectionHeader.X = p.ScreenCenterX - p.sectionHeader.W/2
p.sectionHeader.Y = p.AudioStatusToggle.Y - p.verticalMargin - p.sectionHeader.H
p.listItemHeight = p.audioFileTextures[0].H
listTop := p.sectionHeader.Y - p.listItemMargin
listBottomMargin := int32(12)
listH := listTop - listBottomMargin
if listH < p.listItemHeight {
listH = p.listItemHeight
}
p.listAreaTop = listTop
p.listAreaBottom = listBottomMargin
itemStep := p.listItemHeight + p.listItemMargin
n := len(p.audioFileTextures)
p.maxVisible = int(listH / itemStep)
if p.maxVisible < 1 {
p.maxVisible = 1
}
if p.maxVisible > n {
p.maxVisible = n
}
var maxItemW int32
for i := range p.audioFileTextures {
w := p.audioFileTextures[i].W
if w > maxItemW {
maxItemW = w
}
}
p.listLeftX = p.ScreenCenterX - maxItemW/2
firstVisibleY := p.listAreaTop - p.listItemHeight
for i := range p.audioFileTextures {
item := &p.audioFileTextures[i]
item.X = p.listLeftX
item.Y = firstVisibleY - int32(i-p.scrollOffset)*itemStep
}
maxScroll := n - p.maxVisible
if maxScroll < 0 {
maxScroll = 0
}
if p.scrollOffset > maxScroll {
p.scrollOffset = maxScroll
}
p.scrollTrackY = p.listAreaBottom
p.scrollTrackH = p.listAreaTop - p.listAreaBottom
p.updateCursorPosition()
}
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.audioFileTextures):
item = &p.audioFileTextures[p.focusState]
default:
return
}
p.triX = float32(item.X) - triangleSize - float32(cursorGap)
p.triCenterY = float32(item.Y) + float32(item.H)/2
}
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()
}
func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
e, ok := (*event).(*sdl.KeyboardEvent)
if !ok || e.Type != sdl.KEYDOWN {
return nil
}
n := len(p.audioFileTextures)
switch e.Keysym.Sym {
case sdl.K_RETURN:
if p.focusState == focusToggle {
p.ToggleAudio()
} else if p.focusState >= 0 && p.focusState < n {
p.selectAudioFile(p.focusState)
p.updateCursorPosition()
}
case sdl.K_DOWN:
if p.focusState == focusToggle {
if n > 0 {
p.focusState = 0
p.scrollOffset = 0
p.layout()
}
} else if p.focusState < n-1 {
p.focusState++
if p.focusState >= p.scrollOffset+p.maxVisible {
p.scrollOffset++
p.layout()
} else {
p.updateCursorPosition()
}
}
case sdl.K_UP:
if p.focusState == 0 {
p.focusState = focusToggle
p.updateCursorPosition()
} else if p.focusState > 0 {
p.focusState--
if p.focusState < p.scrollOffset {
p.scrollOffset--
p.layout()
} else {
p.updateCursorPosition()
}
}
case sdl.K_PAGEUP:
if p.focusState > 0 {
p.focusState -= p.maxVisible - 1
if p.focusState < 0 {
p.focusState = 0
}
p.scrollOffset = p.focusState
p.layout()
}
case sdl.K_PAGEDOWN:
if p.focusState < n-1 {
p.focusState += p.maxVisible - 1
if p.focusState >= n {
p.focusState = n - 1
}
p.scrollOffset = p.focusState - p.maxVisible + 1
if p.scrollOffset < 0 {
p.scrollOffset = 0
}
p.layout()
}
case sdl.K_RIGHT:
p.UpdateVolume(10)
case sdl.K_LEFT:
p.UpdateVolume(-10)
case sdl.K_BACKSPACE:
p.Context.NavigateTo(SETTINGS)
}
return nil
}
func (p *AudioOptions) Update(dt float32) error {
p.spinElapsed += dt
if p.spinElapsed >= 1.0 {
p.spinElapsed -= 1.0
}
return nil
}
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}},
}
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
}
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
}
func (p *AudioOptions) renderScrollbar() {
n := len(p.audioFileTextures)
if n <= p.maxVisible {
return
}
trackColor := sdl.Color{R: 255, G: 255, B: 255, A: 30}
thumbColor := sdl.Color{R: 255, G: 255, B: 255, A: 100}
p.RenderSolidColorQuad(p.solidShader,
mgl32.Vec2{float32(p.scrollTrackX), float32(p.scrollTrackY)},
mgl32.Vec2{float32(p.scrollTrackWidth), float32(p.scrollTrackH)},
trackColor)
visibleRatio := float32(p.maxVisible) / float32(n)
scrollRatio := float32(p.scrollOffset) / float32(n)
thumbHeight := float32(p.scrollTrackH) * visibleRatio
if thumbHeight < 20 {
thumbHeight = 20
}
thumbY := float32(p.scrollTrackY) + float32(p.scrollTrackH)*scrollRatio
p.RenderSolidColorQuad(p.solidShader,
mgl32.Vec2{float32(p.scrollTrackX + 2), thumbY},
mgl32.Vec2{float32(p.scrollTrackWidth - 4), thumbHeight},
thumbColor)
}
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)
}
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)
}
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)
}
p.RenderTexture(p.textShader, p.sectionHeader.ID, p.sectionHeader.Position(), p.sectionHeader.RawDimensions(), p.sectionHeader.Color)
// List background — centered width matching widest item + padding
var maxItemW int32
for i := range p.audioFileTextures {
w := p.audioFileTextures[i].W
if w > maxItemW {
maxItemW = w
}
}
bgPad := int32(16)
bgW := maxItemW + 2*bgPad
bgLeft := float32(p.ScreenCenterX) - float32(bgW)/2
bgTop := float32(p.listAreaBottom)
bgBottom := float32(p.listAreaTop)
listBgColor := sdl.Color{R: 255, G: 255, B: 255, A: 12}
p.RenderSolidColorQuad(p.solidShader,
mgl32.Vec2{bgLeft, bgTop},
mgl32.Vec2{float32(bgW), bgBottom - bgTop},
listBgColor)
// Audio file list (visible window only)
for i := p.scrollOffset; i < p.scrollOffset+p.maxVisible && i < len(p.audioFileTextures); i++ {
item := &p.audioFileTextures[i]
clr := colors.White
if i == p.selectedFileIndex {
clr = colors.Green
}
if err := p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), clr); err != nil {
return fmt.Errorf("render audio file %d: %w", i, err)
}
if i == p.selectedFileIndex {
dotX := float32(item.X) - p.dotSize - 6
dotY := float32(item.Y) + float32(item.H)/2 - p.dotSize/2
p.RenderSolidColorQuad(p.solidShader,
mgl32.Vec2{dotX, dotY},
mgl32.Vec2{p.dotSize, p.dotSize},
colors.Green)
}
}
p.renderScrollbar()
if err := p.renderCursor(); err != nil {
return fmt.Errorf("render cursor: %w", err)
}
return nil
}
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)
gl.DeleteTextures(1, &p.sectionHeader.ID)
for i := range p.audioFileTextures {
gl.DeleteTextures(1, &p.audioFileTextures[i].ID)
}
gl.DeleteBuffers(1, &p.triVBO)
gl.DeleteVertexArrays(1, &p.triVAO)
return nil
}