Replace AudioFile with AudioTrack, fix download+orphan+MD5 sync, always play original audio fallback
AudioTrack table replaces AudioFile (Filename, Name, AssignedFrequency,
MD5, Selected, Order) — fixes relative-path bug in scrollerPage.go that
caused "file not found" for single-track selection.
Download goroutine now:
- Accepts *gorm.DB and updates AudioTrack.MD5 after each download.
- Treats checksums.md5 as source of truth: removes local files and DB
records not in the manifest (orphans).
- Strips "./" prefix from manifest filenames (GNU md5sum format).
AudioOptions.go seeds 14 AudioTrack records matching server checksums.md5.
List background quad width matches the widest item (+ padding), centered.
scrollerPage.go switchAudioForFrequency falls back to original/selected
audio when a frequency has no matching track or file. playOriginalAudio
is a no-op on repeat calls to avoid 5-second audio restart loop.
Fixes progress display: passes Max=0 during downloads so Home page shows
"x / X Files Downloaded" text instead of bogus "100.0%" percentage.
Downstream audio switching (DynamicAudio) verifies an AudioTrack exists
for the frequency before assigning it to a stream — frequencies without
a track (e.g. 528 Hz) gracefully use original audio instead.
This commit is contained in:
@@ -38,6 +38,7 @@ type ScrollerPage struct {
|
||||
|
||||
audioRetryTimer float32
|
||||
audioStarted bool
|
||||
originalAudioStarted bool
|
||||
dynamicAudio bool
|
||||
|
||||
lines *LineDeque
|
||||
@@ -284,34 +285,32 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
|
||||
logger.Info("DynamicAudio mode: %v", p.dynamicAudio)
|
||||
|
||||
// Start audio for the first proclamation in the stream.
|
||||
started := false
|
||||
if len(p.streams) > 0 {
|
||||
if p.dynamicAudio {
|
||||
started = p.switchAudioForFrequency(p.streams[0].frequencyValue)
|
||||
}
|
||||
if len(p.streams) > 0 && p.dynamicAudio {
|
||||
p.switchAudioForFrequency(p.streams[0].frequencyValue)
|
||||
p.audioStarted = true
|
||||
}
|
||||
|
||||
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
|
||||
if err := p.Context.DbPtr.Where("Selected = ?", true).First(&selectedAudio).Error; err != nil {
|
||||
selectedAudio = db.AudioFile{
|
||||
Name: "OriginalAudio",
|
||||
Path: filepath.Join(audioDir(), "OriginalAudio.wav"),
|
||||
Selected: true,
|
||||
Order: 0,
|
||||
if !p.audioStarted && !p.dynamicAudio {
|
||||
// Static track selected in Audio Options. Play whatever track is
|
||||
// marked Selected in the audio_tracks table.
|
||||
var selectedTrack db.AudioTrack
|
||||
if err := p.Context.DbPtr.Where("selected = ?", true).First(&selectedTrack).Error; err != nil {
|
||||
logger.Warning("No selected audio track found, audio will start when a track is chosen")
|
||||
} else {
|
||||
path := filepath.Join(audioDir(), selectedTrack.Filename)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
logger.Info("Playing %s", path)
|
||||
if err := mixer.Play(path, true, config); err != nil {
|
||||
logger.Error("Mixer returned an error: %v", err)
|
||||
} else {
|
||||
p.audioStarted = true
|
||||
}
|
||||
} else {
|
||||
logger.Warning("Selected audio file %s not found yet (download may still be in progress)", path)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Playing %s", selectedAudio.Path)
|
||||
if err := mixer.Play(selectedAudio.Path, true, config); err != nil {
|
||||
logger.Error("Mixer returned an error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
p.audioStarted = started
|
||||
|
||||
currentDevice := mixer.CurrentDevice()
|
||||
if currentDevice == "" {
|
||||
currentDevice = "default"
|
||||
@@ -438,36 +437,55 @@ func audioDir() string {
|
||||
return filepath.Join(home, ".radiantwave", "audio")
|
||||
}
|
||||
|
||||
// audioPathForFrequency builds the on-disk path for a solfeggio frequency's
|
||||
// audio file, e.g. 396 -> "~/.radiantwave/audio/396Hz.wav".
|
||||
func (p *ScrollerPage) audioPathForFrequency(freq int) string {
|
||||
return filepath.Join(audioDir(), fmt.Sprintf("%dHz.wav", freq))
|
||||
}
|
||||
|
||||
// switchAudioForFrequency swaps the currently playing track to match freq.
|
||||
// If freq is 0 or no audio track exists for it, falls back to the original
|
||||
// / selected audio track (so audio never goes silent in Ready state).
|
||||
//
|
||||
// - 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).
|
||||
// Returns true if audio is now playing (either frequency-specific or fallback).
|
||||
func (p *ScrollerPage) switchAudioForFrequency(freq int) bool {
|
||||
if freq == 0 {
|
||||
return false
|
||||
}
|
||||
if freq == p.currentAudioFreq {
|
||||
if freq != 0 && freq == p.currentAudioFreq {
|
||||
return true
|
||||
}
|
||||
|
||||
path := p.audioPathForFrequency(freq)
|
||||
if freq != 0 {
|
||||
if track, err := p.audioTrackForFrequency(freq); err == nil {
|
||||
path := filepath.Join(audioDir(), track.Filename)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
config, err := db.GetConfig(p.Context.DbPtr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mixer.Stop(3000, config)
|
||||
if err := mixer.Play(path, true, config); err != nil {
|
||||
logger.Error("Mixer returned an error: %v", err)
|
||||
return false
|
||||
}
|
||||
p.currentAudioFreq = freq
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return p.playOriginalAudio()
|
||||
}
|
||||
|
||||
// playOriginalAudio plays the selected AudioTrack, or the OriginalAudio track
|
||||
// if nothing is selected. Returns true if audio started successfully.
|
||||
func (p *ScrollerPage) playOriginalAudio() bool {
|
||||
if p.originalAudioStarted {
|
||||
return true
|
||||
}
|
||||
|
||||
var track db.AudioTrack
|
||||
if err := p.Context.DbPtr.Where("selected = ?", true).First(&track).Error; err != nil {
|
||||
if err := p.Context.DbPtr.Where("assigned_frequency = ?", 0).First(&track).Error; err != nil {
|
||||
logger.Warning("No audio tracks available")
|
||||
return false
|
||||
}
|
||||
}
|
||||
path := filepath.Join(audioDir(), track.Filename)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
logger.Warning("No audio file for %dHz (%s), keeping current track", freq, path)
|
||||
logger.Warning("Original audio file not found: %s", path)
|
||||
return false
|
||||
}
|
||||
config, err := db.GetConfig(p.Context.DbPtr)
|
||||
@@ -479,10 +497,17 @@ func (p *ScrollerPage) switchAudioForFrequency(freq int) bool {
|
||||
logger.Error("Mixer returned an error: %v", err)
|
||||
return false
|
||||
}
|
||||
p.currentAudioFreq = freq
|
||||
p.originalAudioStarted = true
|
||||
p.currentAudioFreq = 0
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ScrollerPage) audioTrackForFrequency(freq int) (db.AudioTrack, error) {
|
||||
var track db.AudioTrack
|
||||
err := p.Context.DbPtr.Where("assigned_frequency = ?", freq).First(&track).Error
|
||||
return track, err
|
||||
}
|
||||
|
||||
// nextLineWords gets words for the current line and advances proclamation stream when needed.
|
||||
func (p *ScrollerPage) nextLineWords() []string {
|
||||
if len(p.streams) == 0 {
|
||||
@@ -645,20 +670,29 @@ func (p *ScrollerPage) HandleEvent(event *sdl.Event) error {
|
||||
func (p *ScrollerPage) Update(dt float32) error {
|
||||
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 {
|
||||
// Periodically try to switch to the frequency-specific file in case
|
||||
// it was just downloaded (only when the stream's frequency differs from
|
||||
// whatever is currently playing).
|
||||
if p.dynamicAudio && len(p.streams) > 0 {
|
||||
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
|
||||
freq := p.streams[p.streamIndex].frequencyValue
|
||||
if freq != 0 && freq != p.currentAudioFreq {
|
||||
p.switchAudioForFrequency(freq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Non-dynamic mode: retry selected track in case download just finished.
|
||||
if !p.dynamicAudio && !p.audioStarted {
|
||||
p.audioRetryTimer += dt
|
||||
if p.audioRetryTimer >= 5.0 {
|
||||
p.audioRetryTimer = 0
|
||||
p.audioStarted = p.playOriginalAudio()
|
||||
}
|
||||
}
|
||||
|
||||
p.scrollOffset += dt * p.velocity
|
||||
move := int32(p.scrollOffset)
|
||||
if move == 0 {
|
||||
|
||||
Reference in New Issue
Block a user