Auto play audio based on solfeggio frequencies

This commit is contained in:
NeilClack
2026-06-19 00:11:48 -05:00
parent 9ee7e8021a
commit 5bd2b75267
+109 -42
View File
@@ -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 -> "<AssetsDir>/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 {