Auto play audio based on solfeggio frequencies
This commit is contained in:
+109
-42
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user