Auto play audio based on solfeggio frequencies
This commit is contained in:
+109
-42
@@ -3,6 +3,8 @@ package page
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-gl/gl/v3.3-core/gl"
|
"github.com/go-gl/gl/v3.3-core/gl"
|
||||||
@@ -29,6 +31,11 @@ type ScrollerPage struct {
|
|||||||
streams []proclamationStream
|
streams []proclamationStream
|
||||||
streamIndex int
|
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
|
IsDownloadingAudio bool
|
||||||
|
|
||||||
lines *LineDeque
|
lines *LineDeque
|
||||||
@@ -85,6 +92,11 @@ type proclamationStream struct {
|
|||||||
wordIndex int
|
wordIndex int
|
||||||
palette []sdl.Color // Color palette for this proclamation
|
palette []sdl.Color // Color palette for this proclamation
|
||||||
colorIndex int // Track which color to use next
|
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.
|
// 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
|
// Start audio for the first proclamation in the stream. Falls back to
|
||||||
|
// whatever track is marked Selected in the audio_files table if the
|
||||||
if err := p.Context.DbPtr.Where("Selected = ?", true).First(&selectedAudio).Error; err != nil {
|
// first stream has no frequency (e.g. only the default proclamation is
|
||||||
logger.Error("error querying database for selected audio: %v", err)
|
// 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 !started {
|
||||||
if err := mixer.Play(selectedAudio.Path, true, p.Context.CfgPtr); err != nil {
|
var selectedAudio db.AudioFile
|
||||||
logger.Error("Mixer returned an error: %v", err)
|
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()
|
currentDevice := mixer.CurrentDevice()
|
||||||
@@ -350,20 +372,22 @@ func (p *ScrollerPage) loadProclamations() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.streams = append(p.streams, proclamationStream{
|
p.streams = append(p.streams, proclamationStream{
|
||||||
name: proclamation.Name,
|
name: proclamation.Name,
|
||||||
words: words,
|
words: words,
|
||||||
repeat: proclamationPlayCount,
|
repeat: proclamationPlayCount,
|
||||||
palette: palette,
|
palette: palette,
|
||||||
colorIndex: 0,
|
colorIndex: 0,
|
||||||
|
frequencyValue: freqValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(defaultWords) > 0 {
|
if len(defaultWords) > 0 {
|
||||||
p.streams = append(p.streams, proclamationStream{
|
p.streams = append(p.streams, proclamationStream{
|
||||||
name: defaultProclamation.Name,
|
name: defaultProclamation.Name,
|
||||||
words: defaultWords,
|
words: defaultWords,
|
||||||
repeat: 1,
|
repeat: 1,
|
||||||
palette: colors.Freq_528_Colors[:], // Default palette for default proclamation
|
palette: colors.Freq_528_Colors[:], // Default palette for default proclamation
|
||||||
colorIndex: 0,
|
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.
|
// Fallback: if no non-default selected proclamations, at least play default.
|
||||||
if len(p.streams) == 0 && len(defaultWords) > 0 {
|
if len(p.streams) == 0 && len(defaultWords) > 0 {
|
||||||
p.streams = append(p.streams, proclamationStream{
|
p.streams = append(p.streams, proclamationStream{
|
||||||
name: defaultProclamation.Name,
|
name: defaultProclamation.Name,
|
||||||
words: defaultWords,
|
words: defaultWords,
|
||||||
repeat: 1,
|
repeat: 1,
|
||||||
palette: colors.Freq_528_Colors[:],
|
palette: colors.Freq_528_Colors[:],
|
||||||
colorIndex: 0,
|
colorIndex: 0,
|
||||||
|
frequencyValue: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,6 +412,48 @@ func (p *ScrollerPage) loadProclamations() error {
|
|||||||
return nil
|
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.
|
// nextLineWords gets words for the current line and advances proclamation stream when needed.
|
||||||
func (p *ScrollerPage) nextLineWords() []string {
|
func (p *ScrollerPage) nextLineWords() []string {
|
||||||
if len(p.streams) == 0 {
|
if len(p.streams) == 0 {
|
||||||
@@ -409,13 +476,13 @@ func (p *ScrollerPage) nextLineWords() []string {
|
|||||||
if targetRepeats <= 0 {
|
if targetRepeats <= 0 {
|
||||||
targetRepeats = 1
|
targetRepeats = 1
|
||||||
}
|
}
|
||||||
logger.Info("Now playing proclamation: %s", s.name)
|
|
||||||
if s.completed >= targetRepeats {
|
if s.completed >= targetRepeats {
|
||||||
s.completed = 0
|
s.completed = 0
|
||||||
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)
|
||||||
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 {
|
func (p *ScrollerPage) HandleEvent(event *sdl.Event) error {
|
||||||
switch e := (*event).(type) {
|
// switch e := (*event).(type) {
|
||||||
case *sdl.KeyboardEvent:
|
// case *sdl.KeyboardEvent:
|
||||||
if e.Type != sdl.KEYDOWN {
|
// if e.Type != sdl.KEYDOWN {
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
switch e.Keysym.Sym {
|
// switch e.Keysym.Sym {
|
||||||
case sdl.K_UP:
|
// case sdl.K_UP:
|
||||||
if !p.autoVelocity {
|
// if !p.autoVelocity {
|
||||||
p.velocity += p.increment
|
// p.velocity += p.increment
|
||||||
}
|
// }
|
||||||
case sdl.K_DOWN:
|
// case sdl.K_DOWN:
|
||||||
if !p.autoVelocity {
|
// if !p.autoVelocity {
|
||||||
p.velocity -= p.increment
|
// p.velocity -= p.increment
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,7 +717,7 @@ func (p *ScrollerPage) Destroy() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
mixer.Stop(1000, p.Context.CfgPtr)
|
mixer.Stop(3000, p.Context.CfgPtr)
|
||||||
|
|
||||||
// Delete each cached word texture exactly once.
|
// Delete each cached word texture exactly once.
|
||||||
for k, item := range p.wordTextureCache {
|
for k, item := range p.wordTextureCache {
|
||||||
|
|||||||
Reference in New Issue
Block a user