diff --git a/internal/page/scrollerPage.go b/internal/page/scrollerPage.go index 46ef508..6f128e5 100644 --- a/internal/page/scrollerPage.go +++ b/internal/page/scrollerPage.go @@ -3,6 +3,8 @@ package page import ( "fmt" "math" + "os" + "path/filepath" "strings" "github.com/go-gl/gl/v3.3-core/gl" @@ -29,6 +31,11 @@ type ScrollerPage struct { streams []proclamationStream streamIndex int + // currentAudioFreq is the solfeggio Hz value of whatever track is + // currently playing. 0 means "no specific frequency" (either the + // legacy Selected-AudioFile track, or nothing yet). + currentAudioFreq int + IsDownloadingAudio bool lines *LineDeque @@ -85,6 +92,11 @@ type proclamationStream struct { wordIndex int palette []sdl.Color // Color palette for this proclamation colorIndex int // Track which color to use next + + // frequencyValue is the solfeggio Hz value that should be audible while + // this stream plays. 0 means "no specific frequency" (the default + // proclamation) — entering this stream never changes the audio. + frequencyValue int } // LineDeque is a double-ended queue for Line values backed by a circular buffer. @@ -262,15 +274,25 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { } } - var selectedAudio db.AudioFile - - if err := p.Context.DbPtr.Where("Selected = ?", true).First(&selectedAudio).Error; err != nil { - logger.Error("error querying database for selected audio: %v", err) + // Start audio for the first proclamation in the stream. Falls back to + // whatever track is marked Selected in the audio_files table if the + // first stream has no frequency (e.g. only the default proclamation is + // queued) or its matching file doesn't exist on disk yet. + started := false + if len(p.streams) > 0 { + started = p.switchAudioForFrequency(p.streams[0].frequencyValue) } - logger.Info("Playing %s", selectedAudio.Path) - if err := mixer.Play(selectedAudio.Path, true, p.Context.CfgPtr); err != nil { - logger.Error("Mixer returned an error: %v", err) + if !started { + var selectedAudio db.AudioFile + if err := p.Context.DbPtr.Where("Selected = ?", true).First(&selectedAudio).Error; err != nil { + logger.Error("error querying database for selected audio: %v", err) + } + + logger.Info("Playing %s", selectedAudio.Path) + if err := mixer.Play(selectedAudio.Path, true, p.Context.CfgPtr); err != nil { + logger.Error("Mixer returned an error: %v", err) + } } currentDevice := mixer.CurrentDevice() @@ -350,20 +372,22 @@ func (p *ScrollerPage) loadProclamations() error { } p.streams = append(p.streams, proclamationStream{ - name: proclamation.Name, - words: words, - repeat: proclamationPlayCount, - palette: palette, - colorIndex: 0, + name: proclamation.Name, + words: words, + repeat: proclamationPlayCount, + palette: palette, + colorIndex: 0, + frequencyValue: freqValue, }) if len(defaultWords) > 0 { p.streams = append(p.streams, proclamationStream{ - name: defaultProclamation.Name, - words: defaultWords, - repeat: 1, - palette: colors.Freq_528_Colors[:], // Default palette for default proclamation - colorIndex: 0, + name: defaultProclamation.Name, + words: defaultWords, + repeat: 1, + palette: colors.Freq_528_Colors[:], // Default palette for default proclamation + colorIndex: 0, + frequencyValue: 0, // default proclamation never changes audio }) } } @@ -371,11 +395,12 @@ func (p *ScrollerPage) loadProclamations() error { // Fallback: if no non-default selected proclamations, at least play default. if len(p.streams) == 0 && len(defaultWords) > 0 { p.streams = append(p.streams, proclamationStream{ - name: defaultProclamation.Name, - words: defaultWords, - repeat: 1, - palette: colors.Freq_528_Colors[:], - colorIndex: 0, + name: defaultProclamation.Name, + words: defaultWords, + repeat: 1, + palette: colors.Freq_528_Colors[:], + colorIndex: 0, + frequencyValue: 0, }) } @@ -387,6 +412,48 @@ func (p *ScrollerPage) loadProclamations() error { return nil } +// audioPathForFrequency builds the on-disk path for a solfeggio frequency's +// audio file, e.g. 396 -> "/audio/396Hz.wav". +func (p *ScrollerPage) audioPathForFrequency(freq int) string { + return filepath.Join(p.Context.CfgPtr.AssetsDir, "audio", fmt.Sprintf("%dHz.wav", freq)) +} + +// switchAudioForFrequency swaps the currently playing track to match freq. +// +// - freq == 0 ("no specific frequency", i.e. the default proclamation) is +// always a no-op — whatever is playing keeps playing. +// - If freq already matches what's playing, nothing happens. +// - If the matching file doesn't exist on disk yet, the current track +// keeps playing untouched (so missing solfeggio audio never causes +// silence — and starts working automatically once the file is added). +// - Otherwise the current track is stopped immediately and freq's track +// starts right away (hard cut, no fade). +// +// Returns true if freq's track is now playing (either just switched, or +// already was). +func (p *ScrollerPage) switchAudioForFrequency(freq int) bool { + if freq == 0 { + return false + } + if freq == p.currentAudioFreq { + return true + } + + path := p.audioPathForFrequency(freq) + if _, err := os.Stat(path); err != nil { + logger.Warning("No audio file for %dHz (%s), keeping current track", freq, path) + return false + } + + mixer.Stop(3000, p.Context.CfgPtr) + if err := mixer.Play(path, true, p.Context.CfgPtr); err != nil { + logger.Error("Mixer returned an error: %v", err) + return false + } + p.currentAudioFreq = freq + return true +} + // nextLineWords gets words for the current line and advances proclamation stream when needed. func (p *ScrollerPage) nextLineWords() []string { if len(p.streams) == 0 { @@ -409,13 +476,13 @@ func (p *ScrollerPage) nextLineWords() []string { if targetRepeats <= 0 { targetRepeats = 1 } - logger.Info("Now playing proclamation: %s", s.name) if s.completed >= targetRepeats { s.completed = 0 s.wordIndex = 0 s.colorIndex = 0 // Reset color index when proclamation restarts p.streamIndex = (p.streamIndex + 1) % len(p.streams) - fmt.Println("Now playing proclamation:", p.streams[p.streamIndex].name) + next := &p.streams[p.streamIndex] + p.switchAudioForFrequency(next.frequencyValue) } } @@ -523,23 +590,23 @@ func (p *ScrollerPage) createLine(index int, isInitial bool) error { } func (p *ScrollerPage) HandleEvent(event *sdl.Event) error { - switch e := (*event).(type) { - case *sdl.KeyboardEvent: - if e.Type != sdl.KEYDOWN { - return nil - } - switch e.Keysym.Sym { - case sdl.K_UP: - if !p.autoVelocity { - p.velocity += p.increment - } - case sdl.K_DOWN: - if !p.autoVelocity { - p.velocity -= p.increment - } - } - return nil - } + // switch e := (*event).(type) { + // case *sdl.KeyboardEvent: + // if e.Type != sdl.KEYDOWN { + // return nil + // } + // switch e.Keysym.Sym { + // case sdl.K_UP: + // if !p.autoVelocity { + // p.velocity += p.increment + // } + // case sdl.K_DOWN: + // if !p.autoVelocity { + // p.velocity -= p.increment + // } + // } + // return nil + // } return nil } @@ -650,7 +717,7 @@ func (p *ScrollerPage) Destroy() error { return nil } - mixer.Stop(1000, p.Context.CfgPtr) + mixer.Stop(3000, p.Context.CfgPtr) // Delete each cached word texture exactly once. for k, item := range p.wordTextureCache {