Add audio list

This commit is contained in:
NeilClack
2026-07-27 13:14:37 -05:00
parent 5854c57f6f
commit b1a5a670ab
4 changed files with 162 additions and 120 deletions
+113 -106
View File
@@ -16,12 +16,10 @@ import (
)
// 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.
// row has keyboard focus. Audio-file 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},
@@ -41,19 +39,12 @@ var AudioFileList = []db.AudioFile{
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
@@ -66,11 +57,9 @@ type AudioOptions struct {
// Audio toggle
AudioStatusToggle StringItem
// Audio Devices
activeDeviceIndex int
NumAudioDevices int
AudioDevices []AudioDevice
AudioDeviceTextures []StringItem
// Audio file list — index 0 = "Dynamic Audio", index 1+ = AudioFileList entries
audioFileTextures []StringItem
selectedFileIndex int // which item is currently selected (highlights green)
// Volume row
VolumeLabel StringItem
@@ -87,7 +76,7 @@ type AudioOptions struct {
triCenterY float32
spinElapsed float32
// Focus: focusToggle (-1) = toggle row, 0..n-1 = device at that index
// Focus: focusToggle (-1) = toggle row, 0..n-1 = audio file list item
focusState int
}
@@ -162,37 +151,14 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
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)
}
// Build the audio file list textures: "Dynamic Audio" + AudioFileList
p.buildAudioFileList()
// Triangle cursor VAO/VBO — stride is vec2 only, no tex coords
triVerts := []float32{
0.5, 0.0,
@@ -213,6 +179,72 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
return nil
}
// buildAudioFileList creates textures for the audio file list.
// Index 0 = "Dynamic Audio", index 1+ = AudioFileList entries.
func (p *AudioOptions) buildAudioFileList() {
// Clear old textures
for i := range p.audioFileTextures {
gl.DeleteTextures(1, &p.audioFileTextures[i].ID)
}
p.audioFileTextures = nil
// "Dynamic Audio" entry at index 0
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)
}
// AudioFileList entries at index 1+
for _, af := range AudioFileList {
item, err := NewStringItem(af.Name, p.font, colors.White)
if err != nil {
logger.Error("failed to create audio file string item for %s: %v", af.Name, err)
continue
}
item.Scale(p.ItemScale)
p.audioFileTextures = append(p.audioFileTextures, item)
}
// Determine which item is currently selected
p.updateSelectedIndex()
}
// updateSelectedIndex sets selectedFileIndex based on the DB state.
func (p *AudioOptions) updateSelectedIndex() {
config, err := db.GetConfig(p.Context.DbPtr)
if err != nil {
p.selectedFileIndex = 0 // default to Dynamic Audio
return
}
if config.DynamicAudio {
p.selectedFileIndex = 0
return
}
// Find the audio file with Selected = true
var selected db.AudioFile
if err := p.Context.DbPtr.Where("selected = ?", true).First(&selected).Error; err != nil {
// No selection found — default to Dynamic Audio
p.selectedFileIndex = 0
return
}
// Map the selected AudioFile ID to its index in the list
for i, af := range AudioFileList {
if af.ID == selected.ID {
p.selectedFileIndex = i + 1 // +1 because index 0 is "Dynamic Audio"
return
}
}
// Fallback — file not in list
p.selectedFileIndex = 0
}
// seedAudioFiles ensures the audio_files table has its default rows.
func (p *AudioOptions) seedAudioFiles() error {
var existing []db.AudioFile
@@ -248,61 +280,37 @@ func (p *AudioOptions) CreateAudioStatusItem() error {
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]
// ── Audio file selection ─────────────────────────────────────────────────────
// selectAudioFile handles the user pressing Enter on an audio file list item.
func (p *AudioOptions) selectAudioFile(index int) {
config, err := db.GetConfig(p.Context.DbPtr)
if err != nil {
return fmt.Errorf("failed to get config for device switch: %w", err)
panic(err)
}
if err := mixer.SwitchDevice(device.Label, config); err != nil {
return fmt.Errorf("failed to switch to device %s: %w", device.Label, err)
if index == 0 {
// "Dynamic Audio" selected
config.DynamicAudio = true
// Clear all file selections
p.Context.DbPtr.Model(&db.AudioFile{}).Where("1 = 1").Update("selected", false)
} else {
// Specific audio file selected
config.DynamicAudio = false
fileIndex := index - 1 // offset for "Dynamic Audio" entry
if fileIndex >= 0 && fileIndex < len(AudioFileList) {
// Clear all selections first
p.Context.DbPtr.Model(&db.AudioFile{}).Where("1 = 1").Update("selected", false)
// Select the chosen file
p.Context.DbPtr.Model(&db.AudioFile{}).Where("id = ?", AudioFileList[fileIndex].ID).Update("selected", true)
}
}
// 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)
logger.Error("failed to save audio config: %v", err)
}
p.activeDeviceIndex = deviceIndex
return nil
p.selectedFileIndex = index
}
// ── Volume ────────────────────────────────────────────────────────────────────
@@ -388,10 +396,10 @@ func (p *AudioOptions) layout() {
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
// Audio file list — each item centred and stacked below the toggle
nextY := p.AudioStatusToggle.Y - p.verticalMargin
for i := range p.AudioDeviceTextures {
item := &p.AudioDeviceTextures[i]
for i := range p.audioFileTextures {
item := &p.audioFileTextures[i]
item.X = p.ScreenCenterX - item.W/2
item.Y = nextY - item.H
nextY = item.Y - p.verticalMargin
@@ -410,8 +418,8 @@ func (p *AudioOptions) updateCursorPosition() {
switch {
case p.focusState == focusToggle:
item = &p.AudioStatusToggle
case p.focusState >= 0 && p.focusState < len(p.AudioDeviceTextures):
item = &p.AudioDeviceTextures[p.focusState]
case p.focusState >= 0 && p.focusState < len(p.audioFileTextures):
item = &p.audioFileTextures[p.focusState]
default:
return
}
@@ -446,19 +454,18 @@ func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
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)
}
} else if p.focusState >= 0 && p.focusState < len(p.audioFileTextures) {
p.selectAudioFile(p.focusState)
p.updateCursorPosition()
}
case sdl.K_DOWN:
if p.focusState == focusToggle {
if len(p.AudioDeviceTextures) > 0 {
if len(p.audioFileTextures) > 0 {
p.focusState = 0
p.updateCursorPosition()
}
} else if p.focusState < len(p.AudioDeviceTextures)-1 {
} else if p.focusState < len(p.audioFileTextures)-1 {
p.focusState++
p.updateCursorPosition()
}
@@ -526,7 +533,7 @@ func (p *AudioOptions) renderVolumeBar() error {
{{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
{{bx + bw - thickness, by}, {thickness, bh}},
}
for _, r := range outlineRects {
if err := p.RenderSolidColorQuad(p.solidShader, r[0], r[1], colors.Green); err != nil {
@@ -589,15 +596,15 @@ func (p *AudioOptions) Render() error {
return fmt.Errorf("render audio toggle: %w", err)
}
// Device list
for i := range p.AudioDeviceTextures {
item := &p.AudioDeviceTextures[i]
// Audio file list
for i := range p.audioFileTextures {
item := &p.audioFileTextures[i]
color := colors.White
if i == p.activeDeviceIndex {
if i == p.selectedFileIndex {
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)
return fmt.Errorf("render audio file %d: %w", i, err)
}
}
@@ -617,8 +624,8 @@ func (p *AudioOptions) Destroy() error {
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)
for i := range p.audioFileTextures {
gl.DeleteTextures(1, &p.audioFileTextures[i].ID)
}
gl.DeleteBuffers(1, &p.triVBO)
gl.DeleteVertexArrays(1, &p.triVAO)