Update audio options page to display outputs
This commit is contained in:
@@ -38,17 +38,51 @@ dev_scripts/ # developer utilities
|
||||
### Runtime Data
|
||||
|
||||
Stored in `~/.local/share/rhemawave/`:
|
||||
|
||||
- `data.db` — SQLite database (config, proclamations, logs)
|
||||
- `log.txt` — application log
|
||||
- `license_key` — license key file
|
||||
|
||||
### Development Rules
|
||||
|
||||
These rules must be followed during the development of RhemaWave to retain a
|
||||
maintainable application.
|
||||
|
||||
1. The Application package is soley responsible for managing the database handler, and the
|
||||
configuration cache. Internal packages may update the configuration within the database, and
|
||||
then signal the application to refresh the cache via a callback function `refreshCache()`.
|
||||
configuration cache. Internal packages may update the configuration within the database, and
|
||||
then signal the application to refresh the cache via a callback function `refreshCache()`.
|
||||
|
||||
## Deployment
|
||||
|
||||
Deployed to kiosk hardware via NixOS-Anywhere. See `packaging/` for kiosk system configs.
|
||||
Deployed to kiosk hardware via NixOS.
|
||||
|
||||
## Notes
|
||||
|
||||
### Deployment Pipeline
|
||||
|
||||
A manual walkthough of deploying Radiant Wave updates.
|
||||
For reference later when writing automated actions.
|
||||
|
||||
1. Create a new branch for issue being worked on
|
||||
2. Perform work
|
||||
3. Merge to dev when done
|
||||
4. Build dev with `nix build` to verify no errors
|
||||
5. Checkout KioskConfig dev
|
||||
6. Update rhemawave-dev with `nix flake update rhemawave-dev`
|
||||
7. Build rhemawavekiosk-dev virtual machine
|
||||
8. Test virtual machine for problems
|
||||
9. Build dev-iso
|
||||
10. Install dev-iso to a machine and check for problems
|
||||
11. Checkout `main`
|
||||
12. Merge `dev`
|
||||
13. `nix flake update rhemawave`
|
||||
14. Build virtual machine
|
||||
15. Build iso image
|
||||
16. Build commercial virtual machine
|
||||
17. Build commercial iso image
|
||||
18. Build Raspberry Pi img
|
||||
19. Approve all virtual machines and iso images
|
||||
20. Approve all machines update on their own
|
||||
21. Rename all iso image to "radiantwavekiosk-iso-{system}-{MM-DD-YYYY}-{radiantwave-git-hash}.iso"
|
||||
22. Upload iso images to file server
|
||||
23. Move old iso images to `archives/` on the fileserver
|
||||
|
||||
@@ -385,8 +385,6 @@ func ListDevices() []string {
|
||||
|
||||
// CurrentDevice returns the last requested/opened device name ("" means default).
|
||||
func CurrentDevice() string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return deviceName
|
||||
}
|
||||
|
||||
|
||||
+130
-16
@@ -15,6 +15,10 @@ import (
|
||||
"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.
|
||||
@@ -39,7 +43,8 @@ const triangleSize = float32(16)
|
||||
|
||||
type AudioDevice struct {
|
||||
Label string
|
||||
ID int
|
||||
ID uint32
|
||||
index int
|
||||
}
|
||||
|
||||
type AudioOptions struct {
|
||||
@@ -62,6 +67,8 @@ type AudioOptions struct {
|
||||
AudioStatusToggle StringItem
|
||||
|
||||
// Audio Devices
|
||||
activeDeviceIndex int
|
||||
NumAudioDevices int
|
||||
AudioDevices []AudioDevice
|
||||
AudioDeviceTextures []StringItem
|
||||
|
||||
@@ -79,6 +86,9 @@ type AudioOptions struct {
|
||||
triX float32
|
||||
triCenterY float32
|
||||
spinElapsed float32
|
||||
|
||||
// Focus: focusToggle (-1) = toggle row, 0..n-1 = device at that index
|
||||
focusState int
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
@@ -86,6 +96,7 @@ type AudioOptions struct {
|
||||
func (p *AudioOptions) Init(ctx *PageCtx) error {
|
||||
var err error
|
||||
p.verticalMargin = int32(20)
|
||||
p.focusState = focusToggle
|
||||
|
||||
// Font
|
||||
fm := fontManager.Get()
|
||||
@@ -122,7 +133,10 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
|
||||
p.title.Scale(titleScale)
|
||||
|
||||
// Prompt
|
||||
p.prompt, err = NewStringItem("Enter to toggle audio -- Left/Right to change volume -- Backspace to go back", p.font, colors.White)
|
||||
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)
|
||||
}
|
||||
@@ -143,9 +157,21 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
|
||||
logger.Error("failed to create AudioStatusItem in AudioOptions.Init()")
|
||||
}
|
||||
|
||||
// Make sure the audio_files table is populated. This page no longer
|
||||
// presents any track selection UI, but other parts of the app still
|
||||
// read from this table.
|
||||
// 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()
|
||||
for i, dev := range p.AudioDevices {
|
||||
if dev.Label == currentName {
|
||||
p.activeDeviceIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the audio_files table is populated.
|
||||
if err = p.seedAudioFiles(); err != nil {
|
||||
logger.Error("failed to seed audio files: %v", err)
|
||||
}
|
||||
@@ -202,25 +228,46 @@ func (p *AudioOptions) CreateAudioStatusItem() error {
|
||||
}
|
||||
|
||||
// ── Audio Output Devices ──────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) GetAudioDevices() error {
|
||||
numAudioDevices := sdl.GetNumAudioDevices(false)
|
||||
if numAudioDevices == 0 {
|
||||
return nil
|
||||
}
|
||||
var audioDevices = make([]AudioDevice, numAudioDevices)
|
||||
|
||||
// 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 {
|
||||
audioDevice := sdl.GetAudioDeviceName(i, false)
|
||||
audioDevices = append(audioDevices, AudioDevice{Label: audioDevice, ID: i})
|
||||
label := sdl.GetAudioDeviceName(i, false)
|
||||
audioDevices = append(audioDevices, AudioDevice{Label: label, ID: uint32(i)})
|
||||
}
|
||||
p.AudioDevices = audioDevices
|
||||
|
||||
for i := range audioDevices {
|
||||
newAudioDeviceStringItem, err := NewStringItem(p.AudioDevices[i].Label, p.font, colors.White)
|
||||
item, err := NewStringItem(p.AudioDevices[i].Label, p.font, colors.White)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.AudioDeviceTextures = append(p.AudioDeviceTextures, newAudioDeviceStringItem)
|
||||
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]
|
||||
if err := mixer.SwitchDevice(device.Label); err != nil {
|
||||
return fmt.Errorf("failed to switch to device %s: %w", device.Label, err)
|
||||
}
|
||||
p.activeDeviceIndex = deviceIndex
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -294,10 +341,36 @@ func (p *AudioOptions) layout() {
|
||||
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
|
||||
p.AudioStatusToggle.Y = barY - p.verticalMargin*3 - p.AudioStatusToggle.H
|
||||
|
||||
// Triangle cursor — just outside the left edge of the toggle, vertically centred on it
|
||||
// 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)
|
||||
p.triX = float32(p.AudioStatusToggle.X) - float32(triangleSize) - float32(cursorGap)
|
||||
p.triCenterY = float32(p.AudioStatusToggle.Y) + float32(p.AudioStatusToggle.H)/2
|
||||
|
||||
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 ────────────────────────────────────────────────────────────────────
|
||||
@@ -320,7 +393,33 @@ func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
|
||||
|
||||
switch e.Keysym.Sym {
|
||||
case sdl.K_RETURN:
|
||||
p.ToggleAudio()
|
||||
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)
|
||||
@@ -372,7 +471,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}},
|
||||
{{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 {
|
||||
@@ -382,7 +481,7 @@ func (p *AudioOptions) renderVolumeBar() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderCursor draws the spinning triangle cursor.
|
||||
// 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)))
|
||||
|
||||
@@ -435,6 +534,18 @@ func (p *AudioOptions) Render() error {
|
||||
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)
|
||||
@@ -451,6 +562,9 @@ 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)
|
||||
}
|
||||
gl.DeleteBuffers(1, &p.triVBO)
|
||||
gl.DeleteVertexArrays(1, &p.triVAO)
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user