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:
NeilClack
2026-07-29 16:19:15 -05:00
parent 3480e7594a
commit 91711eef93
8 changed files with 402 additions and 152 deletions
+53 -61
View File
@@ -17,25 +17,25 @@ import (
const focusToggle = -1
var AudioFileList = []db.AudioFile{
{ID: 14, Name: "Original Audio", Path: "OriginalAudio.wav", Selected: false, Order: 1},
{ID: 1, Name: "174 Hz", Path: "174Hz.wav", Selected: false, Order: 2},
{ID: 2, Name: "396 Hz", Path: "396Hz.wav", Selected: false, Order: 3},
{ID: 3, Name: "417 Hz", Path: "417Hz.wav", Selected: false, Order: 4},
{ID: 4, Name: "432 Hz", Path: "432Hz.wav", Selected: false, Order: 5},
{ID: 5, Name: "639 Hz", Path: "639Hz.wav", Selected: false, Order: 6},
{ID: 6, Name: "741 Hz", Path: "741Hz.wav", Selected: false, Order: 7},
{ID: 7, Name: "852 Hz", Path: "852Hz.wav", Selected: false, Order: 8},
{ID: 8, Name: "963 Hz", Path: "963Hz.wav", Selected: false, Order: 9},
{ID: 9, Name: "1111 Hz", Path: "1111Hz.wav", Selected: false, Order: 10},
{ID: 10, Name: "14 Hz Binaural Beat", Path: "14Hz_Binaural.wav", Selected: false, Order: 11},
{ID: 11, Name: "40 Hz Binaural Beat", Path: "40Hz_Binaural.wav", Selected: false, Order: 12},
{ID: 12, Name: "Alpha Waves", Path: "AlphaWaves.wav", Selected: false, Order: 13},
{ID: 13, Name: "Pineal Gland", Path: "PinealGland.wav", Selected: false, Order: 14},
}
const triangleSize = float32(16)
var audioTrackSeed = []db.AudioTrack{
{Filename: "OriginalAudio.wav", Name: "Original Audio", AssignedFrequency: 0, MD5: "d39c2676c315438752ab0c55e9427d74", Order: 1},
{Filename: "174Hz.wav", Name: "174 Hz", AssignedFrequency: 174, MD5: "42abaf332e628b646ac2cc9d1b5385e7", Order: 2},
{Filename: "396Hz.wav", Name: "396 Hz", AssignedFrequency: 396, MD5: "d53d45849b042122a3c177fad95ff499", Order: 3},
{Filename: "417Hz.wav", Name: "417 Hz", AssignedFrequency: 417, MD5: "056a7aae0b53111f5c568ea7ecd1fadb", Order: 4},
{Filename: "432Hz.wav", Name: "432 Hz", AssignedFrequency: 432, MD5: "276c57179372404ff59c35024911468e", Order: 5},
{Filename: "639Hz.wav", Name: "639 Hz", AssignedFrequency: 639, MD5: "4e460148230f4417fef39e9817f75e06", Order: 6},
{Filename: "741Hz.wav", Name: "741 Hz", AssignedFrequency: 741, MD5: "cfbd9b03b539db69bcefdd91cedc9ad1", Order: 7},
{Filename: "852Hz.wav", Name: "852 Hz", AssignedFrequency: 852, MD5: "485700296d017f8a1b0a7583a55a7783", Order: 8},
{Filename: "963Hz.wav", Name: "963 Hz", AssignedFrequency: 963, MD5: "2670e13294daef8490a862c078aa8af4", Order: 9},
{Filename: "1111Hz.wav", Name: "1111 Hz", AssignedFrequency: 1111, MD5: "67bbc7619dc21d0750519113472ca335", Order: 10},
{Filename: "14Hz_Binaural.wav", Name: "14 Hz Binaural Beat", AssignedFrequency: 14, MD5: "807c14866640d2ab27a6f5d7fdd24e77", Order: 11},
{Filename: "40Hz_Binaural.wav", Name: "40 Hz Binaural Beat", AssignedFrequency: 40, MD5: "42d60d55a16db4e9b29da47a5a102f2c", Order: 12},
{Filename: "AlphaWaves.wav", Name: "Alpha Waves", AssignedFrequency: 0, MD5: "07a93fa0e633a00d97b8a5e5a259d6ff", Order: 13},
{Filename: "PinealGland.wav", Name: "Pineal Gland", AssignedFrequency: 0, MD5: "998d74e3826b30086d64ff0df7b9f133", Order: 14},
}
type AudioOptions struct {
Base
font *fontManager.FontEntry
@@ -51,6 +51,7 @@ type AudioOptions struct {
AudioStatusToggle StringItem
audioFileTextures []StringItem
audioTracks []db.AudioTrack
selectedFileIndex int
VolumeLabel StringItem
@@ -68,10 +69,8 @@ type AudioOptions struct {
focusState int
// Section header
sectionHeader StringItem
// Scrolling
scrollOffset int
maxVisible int
listItemMargin int32
@@ -83,10 +82,8 @@ type AudioOptions struct {
scrollTrackY int32
scrollTrackH int32
// Selection dot
dotSize float32
// List left-alignment X (same for all items)
listLeftX int32
}
@@ -158,8 +155,8 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
logger.Error("failed to create AudioStatusItem in AudioOptions.Init()")
}
if err = p.seedAudioFiles(); err != nil {
logger.Error("failed to seed audio files: %v", err)
if err = p.seedAudioTracks(); err != nil {
logger.Error("failed to seed audio tracks: %v", err)
}
p.buildAudioFileList()
@@ -195,6 +192,10 @@ func (p *AudioOptions) buildAudioFileList() {
}
p.audioFileTextures = nil
var tracks []db.AudioTrack
p.Context.DbPtr.Order("`order` ASC").Find(&tracks)
p.audioTracks = tracks
dynamicItem, err := NewStringItem("Dynamic Audio", p.font, colors.White)
if err != nil {
logger.Error("failed to create Dynamic Audio string item: %v", err)
@@ -203,10 +204,10 @@ func (p *AudioOptions) buildAudioFileList() {
p.audioFileTextures = append(p.audioFileTextures, dynamicItem)
}
for _, af := range AudioFileList {
item, err := NewStringItem(af.Name, p.font, colors.White)
for _, track := range tracks {
item, err := NewStringItem(track.Name, p.font, colors.White)
if err != nil {
logger.Error("failed to create audio file string item for %s: %v", af.Name, err)
logger.Error("failed to create audio track string item for %s: %v", track.Name, err)
continue
}
item.Scale(p.ItemScale)
@@ -228,14 +229,14 @@ func (p *AudioOptions) updateSelectedIndex() {
return
}
var selected db.AudioFile
var selected db.AudioTrack
if err := p.Context.DbPtr.Where("selected = ?", true).First(&selected).Error; err != nil {
p.selectedFileIndex = 0
return
}
for i, af := range AudioFileList {
if af.ID == selected.ID {
for i, track := range p.audioTracks {
if track.ID == selected.ID {
p.selectedFileIndex = i + 1
return
}
@@ -244,13 +245,13 @@ func (p *AudioOptions) updateSelectedIndex() {
p.selectedFileIndex = 0
}
func (p *AudioOptions) seedAudioFiles() error {
var existing []db.AudioFile
func (p *AudioOptions) seedAudioTracks() error {
var existing []db.AudioTrack
if err := p.Context.DbPtr.Find(&existing).Error; err != nil {
return err
}
if len(existing) == 0 {
return p.Context.DbPtr.Save(&AudioFileList).Error
return p.Context.DbPtr.Save(&audioTrackSeed).Error
}
return nil
}
@@ -284,13 +285,13 @@ func (p *AudioOptions) selectAudioFile(index int) {
if index == 0 {
config.DynamicAudio = true
p.Context.DbPtr.Model(&db.AudioFile{}).Where("1 = 1").Update("selected", false)
p.Context.DbPtr.Model(&db.AudioTrack{}).Where("1 = 1").Update("selected", false)
} else {
config.DynamicAudio = false
fileIndex := index - 1
if fileIndex >= 0 && fileIndex < len(AudioFileList) {
p.Context.DbPtr.Model(&db.AudioFile{}).Where("1 = 1").Update("selected", false)
p.Context.DbPtr.Model(&db.AudioFile{}).Where("id = ?", AudioFileList[fileIndex].ID).Update("selected", true)
trackIndex := index - 1
if trackIndex >= 0 && trackIndex < len(p.audioTracks) {
p.Context.DbPtr.Model(&db.AudioTrack{}).Where("1 = 1").Update("selected", false)
p.Context.DbPtr.Model(&db.AudioTrack{}).Where("id = ?", p.audioTracks[trackIndex].ID).Update("selected", true)
}
}
@@ -350,15 +351,12 @@ func (p *AudioOptions) layout() {
panic(err)
}
// Title
p.title.X = p.ScreenCenterX - p.title.W/2
p.title.Y = p.ScreenHeight - p.title.H - p.verticalMargin*2
// Prompt
p.prompt.X = p.ScreenCenterX - p.prompt.W/2
p.prompt.Y = p.title.Y - p.verticalMargin - p.prompt.H
// Volume row
barH := int32(float32(config.StandardFontSize) * p.ItemScale)
barW := int32(float32(p.ScreenWidth) * 0.30)
barX := p.ScreenCenterX - barW/2
@@ -376,20 +374,16 @@ func (p *AudioOptions) layout() {
p.volumePercent.X = barX + barW + gap
p.volumePercent.Y = barY + (barH-p.volumePercent.H)/2
// Audio toggle
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
p.AudioStatusToggle.Y = barY - p.verticalMargin*3 - p.AudioStatusToggle.H
// Section header — sits above the toggle, marks the start of the audio track list
p.sectionHeader.X = p.ScreenCenterX - p.sectionHeader.W/2
p.sectionHeader.Y = p.AudioStatusToggle.Y - p.verticalMargin - p.sectionHeader.H
// Scrollable list area boundaries
p.listItemHeight = p.audioFileTextures[0].H
listTop := p.sectionHeader.Y - p.listItemMargin
listBottomMargin := int32(12)
// Available height for the list
listH := listTop - listBottomMargin
if listH < p.listItemHeight {
listH = p.listItemHeight
@@ -398,7 +392,6 @@ func (p *AudioOptions) layout() {
p.listAreaTop = listTop
p.listAreaBottom = listBottomMargin
// How many items fit
itemStep := p.listItemHeight + p.listItemMargin
n := len(p.audioFileTextures)
p.maxVisible = int(listH / itemStep)
@@ -409,16 +402,15 @@ func (p *AudioOptions) layout() {
p.maxVisible = n
}
// Compute left alignment: find the widest item to center the block
var maxItemW int32
for i := range p.audioFileTextures {
if p.audioFileTextures[i].W > maxItemW {
maxItemW = p.audioFileTextures[i].W
w := p.audioFileTextures[i].W
if w > maxItemW {
maxItemW = w
}
}
p.listLeftX = p.ScreenCenterX - maxItemW/2
// Position each item — scrollOffset shifts the whole list up/down
firstVisibleY := p.listAreaTop - p.listItemHeight
for i := range p.audioFileTextures {
item := &p.audioFileTextures[i]
@@ -426,7 +418,6 @@ func (p *AudioOptions) layout() {
item.Y = firstVisibleY - int32(i-p.scrollOffset)*itemStep
}
// Clamp scroll offset so we never show empty space
maxScroll := n - p.maxVisible
if maxScroll < 0 {
maxScroll = 0
@@ -435,7 +426,6 @@ func (p *AudioOptions) layout() {
p.scrollOffset = maxScroll
}
// Scrollbar track position
p.scrollTrackY = p.listAreaBottom
p.scrollTrackH = p.listAreaTop - p.listAreaBottom
@@ -678,19 +668,25 @@ func (p *AudioOptions) Render() error {
return fmt.Errorf("render audio toggle: %w", err)
}
// Section header
p.RenderTexture(p.textShader, p.sectionHeader.ID, p.sectionHeader.Position(), p.sectionHeader.RawDimensions(), p.sectionHeader.Color)
// List background
listBgColor := sdl.Color{R: 255, G: 255, B: 255, A: 12}
// List background — centered width matching widest item + padding
var maxItemW int32
for i := range p.audioFileTextures {
w := p.audioFileTextures[i].W
if w > maxItemW {
maxItemW = w
}
}
bgPad := int32(16)
bgLeft := float32(p.listLeftX - bgPad)
bgW := maxItemW + 2*bgPad
bgLeft := float32(p.ScreenCenterX) - float32(bgW)/2
bgTop := float32(p.listAreaBottom)
bgRight := float32(p.scrollTrackX)
bgBottom := float32(p.listAreaTop)
listBgColor := sdl.Color{R: 255, G: 255, B: 255, A: 12}
p.RenderSolidColorQuad(p.solidShader,
mgl32.Vec2{bgLeft, bgTop},
mgl32.Vec2{bgRight - bgLeft, bgBottom - bgTop},
mgl32.Vec2{float32(bgW), bgBottom - bgTop},
listBgColor)
// Audio file list (visible window only)
@@ -705,7 +701,6 @@ func (p *AudioOptions) Render() error {
return fmt.Errorf("render audio file %d: %w", i, err)
}
// Selection dot — small filled circle left of the item name
if i == p.selectedFileIndex {
dotX := float32(item.X) - p.dotSize - 6
dotY := float32(item.Y) + float32(item.H)/2 - p.dotSize/2
@@ -714,13 +709,10 @@ func (p *AudioOptions) Render() error {
mgl32.Vec2{p.dotSize, p.dotSize},
colors.Green)
}
}
// Scrollbar
p.renderScrollbar()
// Spinning triangle cursor — always on top
if err := p.renderCursor(); err != nil {
return fmt.Errorf("render cursor: %w", err)
}