Add audio list
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
description = "RhemaWave";
|
description = "RadiantWave";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
@@ -26,17 +26,25 @@
|
|||||||
SDL2_ttf
|
SDL2_ttf
|
||||||
SDL2_image
|
SDL2_image
|
||||||
SDL2_mixer
|
SDL2_mixer
|
||||||
libGL
|
|
||||||
sqlite
|
sqlite
|
||||||
dbus
|
dbus
|
||||||
] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [
|
] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [
|
||||||
|
libGL
|
||||||
wayland
|
wayland
|
||||||
wayland-protocols
|
wayland-protocols
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
libdecor
|
libdecor
|
||||||
libx11
|
libx11
|
||||||
libxext
|
libxext
|
||||||
];
|
] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk.frameworks; [
|
||||||
|
OpenGL
|
||||||
|
CoreAudio
|
||||||
|
CoreFoundation
|
||||||
|
CoreServices
|
||||||
|
ForceFeedback
|
||||||
|
IOKit
|
||||||
|
AppKit
|
||||||
|
]);
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
go
|
go
|
||||||
@@ -46,8 +54,8 @@
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages.default = pkgs.buildGoModule {
|
packages.default = pkgs.buildGoModule {
|
||||||
pname = "rhemawave";
|
pname = "radiantwave";
|
||||||
version = "v1.1.1";
|
version = "v1.0.0";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
vendorHash = null;
|
vendorHash = null;
|
||||||
@@ -74,7 +82,7 @@
|
|||||||
meta = with pkgs.lib; {
|
meta = with pkgs.lib; {
|
||||||
description = "RhemaWave kiosk application";
|
description = "RhemaWave kiosk application";
|
||||||
license = licenses.unfree;
|
license = licenses.unfree;
|
||||||
platforms = platforms.linux;
|
platforms = platforms.linux ++ platforms.darwin;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type AppConfig struct {
|
|||||||
StandardFontSize int // The font size used for most text rendering
|
StandardFontSize int // The font size used for most text rendering
|
||||||
LinePattern string // "fibonacci"
|
LinePattern string // "fibonacci"
|
||||||
AudioEnabled bool
|
AudioEnabled bool
|
||||||
|
DynamicAudio bool // When true, scroller switches audio per-proclamation frequency
|
||||||
SavedVolume float64 // Last saved volume as a percentage
|
SavedVolume float64 // Last saved volume as a percentage
|
||||||
FirstRun bool // Is this the first time the application has been run?
|
FirstRun bool // Is this the first time the application has been run?
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-106
@@ -16,12 +16,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// focusToggle is the sentinel focusState value meaning the audio on/off toggle
|
// 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
|
const focusToggle = -1
|
||||||
|
|
||||||
// AudioFileList is kept as the canonical seed data for the audio_files table.
|
// 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{
|
var AudioFileList = []db.AudioFile{
|
||||||
{ID: 14, Name: "Original Audio", Path: "OriginalAudio.wav", Selected: false, Order: 1},
|
{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: 1, Name: "174 Hz", Path: "174Hz.wav", Selected: false, Order: 2},
|
||||||
@@ -41,19 +39,12 @@ var AudioFileList = []db.AudioFile{
|
|||||||
|
|
||||||
const triangleSize = float32(16)
|
const triangleSize = float32(16)
|
||||||
|
|
||||||
type AudioDevice struct {
|
|
||||||
Label string
|
|
||||||
ID uint32
|
|
||||||
index int
|
|
||||||
}
|
|
||||||
|
|
||||||
type AudioOptions struct {
|
type AudioOptions struct {
|
||||||
// Page Attributes
|
// Page Attributes
|
||||||
Base
|
Base
|
||||||
font *fontManager.FontEntry
|
font *fontManager.FontEntry
|
||||||
textShader *shaderManager.Shader
|
textShader *shaderManager.Shader
|
||||||
solidShader *shaderManager.Shader
|
solidShader *shaderManager.Shader
|
||||||
CfgPtr *db.AppConfig
|
|
||||||
|
|
||||||
// Page Textures
|
// Page Textures
|
||||||
title StringItem
|
title StringItem
|
||||||
@@ -66,11 +57,9 @@ type AudioOptions struct {
|
|||||||
// Audio toggle
|
// Audio toggle
|
||||||
AudioStatusToggle StringItem
|
AudioStatusToggle StringItem
|
||||||
|
|
||||||
// Audio Devices
|
// Audio file list — index 0 = "Dynamic Audio", index 1+ = AudioFileList entries
|
||||||
activeDeviceIndex int
|
audioFileTextures []StringItem
|
||||||
NumAudioDevices int
|
selectedFileIndex int // which item is currently selected (highlights green)
|
||||||
AudioDevices []AudioDevice
|
|
||||||
AudioDeviceTextures []StringItem
|
|
||||||
|
|
||||||
// Volume row
|
// Volume row
|
||||||
VolumeLabel StringItem
|
VolumeLabel StringItem
|
||||||
@@ -87,7 +76,7 @@ type AudioOptions struct {
|
|||||||
triCenterY float32
|
triCenterY float32
|
||||||
spinElapsed 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
|
focusState int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,37 +151,14 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
|
|||||||
logger.Error("failed to create AudioStatusItem in AudioOptions.Init()")
|
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.
|
// Make sure the audio_files table is populated.
|
||||||
if err = p.seedAudioFiles(); err != nil {
|
if err = p.seedAudioFiles(); err != nil {
|
||||||
logger.Error("failed to seed audio files: %v", err)
|
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
|
// Triangle cursor VAO/VBO — stride is vec2 only, no tex coords
|
||||||
triVerts := []float32{
|
triVerts := []float32{
|
||||||
0.5, 0.0,
|
0.5, 0.0,
|
||||||
@@ -213,6 +179,72 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
|
|||||||
return nil
|
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.
|
// seedAudioFiles ensures the audio_files table has its default rows.
|
||||||
func (p *AudioOptions) seedAudioFiles() error {
|
func (p *AudioOptions) seedAudioFiles() error {
|
||||||
var existing []db.AudioFile
|
var existing []db.AudioFile
|
||||||
@@ -248,61 +280,37 @@ func (p *AudioOptions) CreateAudioStatusItem() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Audio Output Devices ──────────────────────────────────────────────────────
|
// ── Audio file selection ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
|
// 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)
|
config, err := db.GetConfig(p.Context.DbPtr)
|
||||||
if err != nil {
|
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 {
|
if index == 0 {
|
||||||
return fmt.Errorf("failed to switch to device %s: %w", device.Label, err)
|
// "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 {
|
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
|
p.selectedFileIndex = index
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Volume ────────────────────────────────────────────────────────────────────
|
// ── Volume ────────────────────────────────────────────────────────────────────
|
||||||
@@ -388,10 +396,10 @@ func (p *AudioOptions) layout() {
|
|||||||
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
|
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
|
||||||
p.AudioStatusToggle.Y = barY - p.verticalMargin*3 - p.AudioStatusToggle.H
|
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
|
nextY := p.AudioStatusToggle.Y - p.verticalMargin
|
||||||
for i := range p.AudioDeviceTextures {
|
for i := range p.audioFileTextures {
|
||||||
item := &p.AudioDeviceTextures[i]
|
item := &p.audioFileTextures[i]
|
||||||
item.X = p.ScreenCenterX - item.W/2
|
item.X = p.ScreenCenterX - item.W/2
|
||||||
item.Y = nextY - item.H
|
item.Y = nextY - item.H
|
||||||
nextY = item.Y - p.verticalMargin
|
nextY = item.Y - p.verticalMargin
|
||||||
@@ -410,8 +418,8 @@ func (p *AudioOptions) updateCursorPosition() {
|
|||||||
switch {
|
switch {
|
||||||
case p.focusState == focusToggle:
|
case p.focusState == focusToggle:
|
||||||
item = &p.AudioStatusToggle
|
item = &p.AudioStatusToggle
|
||||||
case p.focusState >= 0 && p.focusState < len(p.AudioDeviceTextures):
|
case p.focusState >= 0 && p.focusState < len(p.audioFileTextures):
|
||||||
item = &p.AudioDeviceTextures[p.focusState]
|
item = &p.audioFileTextures[p.focusState]
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -446,19 +454,18 @@ func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
|
|||||||
case sdl.K_RETURN:
|
case sdl.K_RETURN:
|
||||||
if p.focusState == focusToggle {
|
if p.focusState == focusToggle {
|
||||||
p.ToggleAudio()
|
p.ToggleAudio()
|
||||||
} else if p.focusState >= 0 && p.focusState < len(p.AudioDevices) {
|
} else if p.focusState >= 0 && p.focusState < len(p.audioFileTextures) {
|
||||||
if err := p.SelectAudioDevice(p.focusState); err != nil {
|
p.selectAudioFile(p.focusState)
|
||||||
logger.Error("SelectAudioDevice: %v", err)
|
p.updateCursorPosition()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case sdl.K_DOWN:
|
case sdl.K_DOWN:
|
||||||
if p.focusState == focusToggle {
|
if p.focusState == focusToggle {
|
||||||
if len(p.AudioDeviceTextures) > 0 {
|
if len(p.audioFileTextures) > 0 {
|
||||||
p.focusState = 0
|
p.focusState = 0
|
||||||
p.updateCursorPosition()
|
p.updateCursorPosition()
|
||||||
}
|
}
|
||||||
} else if p.focusState < len(p.AudioDeviceTextures)-1 {
|
} else if p.focusState < len(p.audioFileTextures)-1 {
|
||||||
p.focusState++
|
p.focusState++
|
||||||
p.updateCursorPosition()
|
p.updateCursorPosition()
|
||||||
}
|
}
|
||||||
@@ -526,7 +533,7 @@ func (p *AudioOptions) renderVolumeBar() error {
|
|||||||
{{bx, by}, {bw, thickness}},
|
{{bx, by}, {bw, thickness}},
|
||||||
{{bx, by + bh - thickness}, {bw, thickness}},
|
{{bx, by + bh - thickness}, {bw, thickness}},
|
||||||
{{bx, by}, {thickness, bh}},
|
{{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 {
|
for _, r := range outlineRects {
|
||||||
if err := p.RenderSolidColorQuad(p.solidShader, r[0], r[1], colors.Green); err != nil {
|
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)
|
return fmt.Errorf("render audio toggle: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device list
|
// Audio file list
|
||||||
for i := range p.AudioDeviceTextures {
|
for i := range p.audioFileTextures {
|
||||||
item := &p.AudioDeviceTextures[i]
|
item := &p.audioFileTextures[i]
|
||||||
color := colors.White
|
color := colors.White
|
||||||
if i == p.activeDeviceIndex {
|
if i == p.selectedFileIndex {
|
||||||
color = colors.Green
|
color = colors.Green
|
||||||
}
|
}
|
||||||
if err := p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), color); err != nil {
|
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.VolumeLabel.ID)
|
||||||
gl.DeleteTextures(1, &p.volumePercent.ID)
|
gl.DeleteTextures(1, &p.volumePercent.ID)
|
||||||
gl.DeleteTextures(1, &p.AudioStatusToggle.ID)
|
gl.DeleteTextures(1, &p.AudioStatusToggle.ID)
|
||||||
for i := range p.AudioDeviceTextures {
|
for i := range p.audioFileTextures {
|
||||||
gl.DeleteTextures(1, &p.AudioDeviceTextures[i].ID)
|
gl.DeleteTextures(1, &p.audioFileTextures[i].ID)
|
||||||
}
|
}
|
||||||
gl.DeleteBuffers(1, &p.triVBO)
|
gl.DeleteBuffers(1, &p.triVBO)
|
||||||
gl.DeleteVertexArrays(1, &p.triVAO)
|
gl.DeleteVertexArrays(1, &p.triVAO)
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ type ScrollerPage struct {
|
|||||||
// legacy Selected-AudioFile track, or nothing yet).
|
// legacy Selected-AudioFile track, or nothing yet).
|
||||||
currentAudioFreq int
|
currentAudioFreq int
|
||||||
|
|
||||||
IsDownloadingAudio bool
|
audioRetryTimer float32
|
||||||
|
audioStarted bool
|
||||||
|
dynamicAudio bool
|
||||||
|
|
||||||
lines *LineDeque
|
lines *LineDeque
|
||||||
lineHeight int32
|
lineHeight int32
|
||||||
@@ -277,16 +279,21 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start audio for the first proclamation in the stream. Falls back to
|
// Read DynamicAudio setting from config.
|
||||||
// whatever track is marked Selected in the audio_files table if the
|
p.dynamicAudio = config.DynamicAudio
|
||||||
// first stream has no frequency (e.g. only the default proclamation is
|
logger.Info("DynamicAudio mode: %v", p.dynamicAudio)
|
||||||
// queued) or its matching file doesn't exist on disk yet.
|
|
||||||
|
// Start audio for the first proclamation in the stream.
|
||||||
started := false
|
started := false
|
||||||
if len(p.streams) > 0 {
|
if len(p.streams) > 0 {
|
||||||
|
if p.dynamicAudio {
|
||||||
started = p.switchAudioForFrequency(p.streams[0].frequencyValue)
|
started = p.switchAudioForFrequency(p.streams[0].frequencyValue)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !started {
|
if !started {
|
||||||
|
// Non-dynamic mode or frequency-specific file not yet available.
|
||||||
|
// Play whatever track is marked Selected in the audio_files table.
|
||||||
var selectedAudio db.AudioFile
|
var selectedAudio db.AudioFile
|
||||||
if err := p.Context.DbPtr.Where("Selected = ?", true).First(&selectedAudio).Error; err != nil {
|
if err := p.Context.DbPtr.Where("Selected = ?", true).First(&selectedAudio).Error; err != nil {
|
||||||
selectedAudio = db.AudioFile{
|
selectedAudio = db.AudioFile{
|
||||||
@@ -303,6 +310,8 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.audioStarted = started
|
||||||
|
|
||||||
currentDevice := mixer.CurrentDevice()
|
currentDevice := mixer.CurrentDevice()
|
||||||
if currentDevice == "" {
|
if currentDevice == "" {
|
||||||
currentDevice = "default"
|
currentDevice = "default"
|
||||||
@@ -501,8 +510,11 @@ func (p *ScrollerPage) nextLineWords() []string {
|
|||||||
s.wordIndex = 0
|
s.wordIndex = 0
|
||||||
s.colorIndex = 0 // Reset color index when proclamation restarts
|
s.colorIndex = 0 // Reset color index when proclamation restarts
|
||||||
p.streamIndex = (p.streamIndex + 1) % len(p.streams)
|
p.streamIndex = (p.streamIndex + 1) % len(p.streams)
|
||||||
|
if p.dynamicAudio {
|
||||||
next := &p.streams[p.streamIndex]
|
next := &p.streams[p.streamIndex]
|
||||||
p.switchAudioForFrequency(next.frequencyValue)
|
result := p.switchAudioForFrequency(next.frequencyValue)
|
||||||
|
p.audioStarted = result || next.frequencyValue == 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,6 +645,20 @@ func (p *ScrollerPage) HandleEvent(event *sdl.Event) error {
|
|||||||
func (p *ScrollerPage) Update(dt float32) error {
|
func (p *ScrollerPage) Update(dt float32) error {
|
||||||
p.updateAutoVelocity(dt)
|
p.updateAutoVelocity(dt)
|
||||||
|
|
||||||
|
// In dynamic mode, retry audio switch every 5 seconds until the
|
||||||
|
// frequency-specific file is found. In non-dynamic mode the selected
|
||||||
|
// file plays continuously — nothing to retry.
|
||||||
|
if p.dynamicAudio && !p.audioStarted {
|
||||||
|
p.audioRetryTimer += dt
|
||||||
|
if p.audioRetryTimer >= 5.0 {
|
||||||
|
p.audioRetryTimer = 0
|
||||||
|
if len(p.streams) > 0 {
|
||||||
|
freq := p.streams[p.streamIndex].frequencyValue
|
||||||
|
p.audioStarted = p.switchAudioForFrequency(freq) || freq == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p.scrollOffset += dt * p.velocity
|
p.scrollOffset += dt * p.velocity
|
||||||
move := int32(p.scrollOffset)
|
move := int32(p.scrollOffset)
|
||||||
if move == 0 {
|
if move == 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user