28717836a47d36bdf25b1f8ae4b3fc65ada011d3
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/README.md b/README.md
2index 7fb07f4124b84bcc2aff183ed468737523306aec..a31d748ea38a412a570e7f8efde072d2dc51f104 100644
3--- a/README.md
4+++ b/README.md
5@@ -11,9 +11,10 @@ you guess the title and artist.
6 types, skips files under any blacklisted directory, and reads each file's title/artist
7 tags (via [`dhowden/tag`](https://github.com/dhowden/tag)). Files without
8 readable tags fall back to the filename as the title.
9-2. **Clip** — decodes the first ~10 seconds of a song to mono PCM with `ffmpeg`,
10- finds the point where the RMS volume first crosses a threshold (so quiet,
11- slow intros are skipped), and plays the following one second via `aplay`.
12+2. **Clip** — decodes the first 30 seconds of a song to mono PCM with `ffmpeg`,
13+ finds the point where the RMS volume first crosses the average volume of
14+ that window (so quiet, slow intros are skipped), and plays the following
15+ one second via `aplay`.
16 3. **Guess** — the TUI shows two input fields (title, artist). You type your
17 answer and submit. Matching ignores casing, punctuation, and extra whitespace,
18 allows typos, and accepts omitted parenthesized extras such as `(Live)`.
19@@ -38,14 +39,13 @@ go build -o museguessr .
20 ## Usage
21
22 ```sh
23-museguessr -dir /path/to/music [-blacklist blacklist.txt] [-threshold 2000] [-rounds 10]
24+museguessr -dir /path/to/music [-blacklist blacklist.txt] [-rounds 10]
25 ```
26
27 | Flag | Default | Description |
28 | ------------ | ------------ | ------------------------------------------------------------------ |
29 | `-dir` | `.` | Directory to scan for music files (searched recursively). |
30 | `-blacklist` | `blacklist` | File with one directory per line; every file under it is excluded. |
31-| `-threshold` | `2000` | RMS level (0–32767) that marks the start of "loud enough" audio. |
32 | `-rounds` | catalog size | Number of songs to play. `0` means one per song in the catalog. |
33
34 ### Blacklist file
35diff --git a/audio.go b/audio.go
36index 842c3884342b0e5a25f61ed6ed645f25f11cc036..a60705ae2ff803b4289f4c3d8ca345483c859ba7 100644
37--- a/audio.go
38+++ b/audio.go
39@@ -39,32 +39,28 @@ func decodeIntro(path string, maxSec int) ([]int16, error) {
40 return samples, nil
41 }
42
43-// findStart returns the sample offset where the RMS level first crosses
44-// threshold, so quiet intros are skipped. It scans in 10ms windows. If the
45-// audio never reaches threshold, it falls back to 10% of the window's peak
46-// RMS, and to 0 if the audio is silent throughout.
47-func findStart(samples []int16, threshold int) int {
48+// findStart returns the sample offset where the RMS level first crosses the
49+// average RMS of the decoded window, so quiet intros are skipped. It scans
50+// in 10ms windows and falls back to 0 if the audio is silent throughout.
51+func findStart(samples []int16) int {
52 win := sampleRate / 100 // 10ms
53 if win < 1 {
54 win = 1
55 }
56
57 rmsVals := make([]float64, 0, len(samples)/win)
58- peak := 0.0
59+ var sum float64
60 for i := 0; i+win <= len(samples); i += win {
61 v := rms(samples[i:i+win])
62 rmsVals = append(rmsVals, v)
63- if v > peak {
64- peak = v
65- }
66+ sum += v
67 }
68-
69- target := float64(threshold)
70- if peak < target {
71- target = peak * 0.1
72+ if len(rmsVals) == 0 {
73+ return 0
74 }
75+ avg := sum / float64(len(rmsVals))
76 for i, v := range rmsVals {
77- if v >= target {
78+ if v >= avg {
79 return i * win
80 }
81 }
82@@ -84,8 +80,8 @@ func rms(s []int16) float64 {
83 }
84
85 // playSegment decodes the intro of songPath, locates where the volume crosses
86-// threshold, and plays the following seconds via aplay.
87-func playSegment(songPath string, threshold, seconds int) error {
88+// the window average, and plays the following seconds via aplay.
89+func playSegment(songPath string, seconds int) error {
90 samples, err := decodeIntro(songPath, introWindow)
91 if err != nil {
92 return err
93@@ -94,7 +90,7 @@ func playSegment(songPath string, threshold, seconds int) error {
94 return fmt.Errorf("no audio decoded from %s", songPath)
95 }
96
97- start := findStart(samples, threshold)
98+ start := findStart(samples)
99 end := start + seconds*sampleRate
100 if end > len(samples) {
101 end = len(samples)
102diff --git a/catalog.go b/catalog.go
103index 75adaaf28d9114fe4c473647cda1d52a646c5a2e..1314e1f73d11548bc52bb112458f8647fd455ba7 100644
104--- a/catalog.go
105+++ b/catalog.go
106@@ -42,8 +42,8 @@ type catalogStats struct {
107 // slow part, so results are cached on disk keyed by file mtime: a fast walk
108 // (stat only) collects the current files, unchanged files reuse their cached
109 // tags, and only new or modified files are re-parsed. onProgress is called
110-// (if non-nil) for every file as it is added to the catalog.
111-func buildCatalog(dir, blacklistPath string, onProgress func(count int, current string)) ([]Song, catalogStats, error) {
112+// (if non-nil) with (count, total) for every file added to the catalog.
113+func buildCatalog(dir, blacklistPath string, onProgress func(count, total int)) ([]Song, catalogStats, error) {
114 absDir, err := filepath.Abs(dir)
115 if err != nil {
116 return nil, catalogStats{}, err
117@@ -84,7 +84,7 @@ func buildCatalog(dir, blacklistPath string, onProgress func(count int, current
118 for _, fr := range current {
119 count++
120 if onProgress != nil {
121- onProgress(count, filepath.Base(fr.path))
122+ onProgress(count, len(current))
123 }
124 if e, ok := cached[fr.path]; ok && e.Mtime == fr.mtime {
125 songs = append(songs, Song{Path: fr.path, Title: e.Title, Artist: e.Artist, Album: e.Album})
126diff --git a/main.go b/main.go
127index 586af4a857cd7d00530ab87a7e7f9a5fc8ae4db8..0644c5b0f3b94123683e31903c58dd1b7a8b51f6 100644
128--- a/main.go
129+++ b/main.go
130@@ -11,13 +11,12 @@ import (
131 func main() {
132 dir := flag.String("dir", "/mnt/media/music", "directory to scan for music files")
133 blacklist := flag.String("blacklist", "blacklist", "path to blacklist file (one directory per line, fully excluded)")
134- threshold := flag.Int("threshold", 2000, "RMS volume threshold (0-32767) used to skip quiet intros")
135 rounds := flag.Int("rounds", 10, "number of rounds")
136 flag.Parse()
137
138 fmt.Printf("Scanning %s …\n", *dir)
139- catalog, stats, err := buildCatalog(*dir, *blacklist, func(n int, current string) {
140- fmt.Printf("\r %d files (now: %s)\x1b[K", n, current)
141+ catalog, stats, err := buildCatalog(*dir, *blacklist, func(n, total int) {
142+ fmt.Printf("\r %d%%\x1b[K", n*100/total)
143 })
144 if err != nil {
145 fmt.Fprintf(os.Stderr, "\nerror building catalog: %v\n", err)
146@@ -33,7 +32,7 @@ func main() {
147 *rounds = len(catalog)
148 }
149
150- m := newModel(catalog, *threshold, *rounds)
151+ m := newModel(catalog, *rounds)
152 p := tea.NewProgram(m, tea.WithAltScreen())
153 if _, err := p.Run(); err != nil {
154 fmt.Fprintf(os.Stderr, "error running TUI: %v\n", err)
155diff --git a/matching_test.go b/matching_test.go
156index ab45bd87b4a840672121ccd408dd1bb47332447e..7d9ed3ad3043262f7f070b2718bb70c561684eca 100644
157--- a/matching_test.go
158+++ b/matching_test.go
159@@ -48,7 +48,7 @@ func TestGuessScoring(t *testing.T) {
160 {"Hello", "Wrong", "21", true, 4, "Title, Album correct! +4 points (halved for 2s hint)"},
161 {"Wrong", "Adele", "21", true, 3, "Artist, Album correct! +3 points (halved for 2s hint)"},
162 } {
163- m := newModel([]Song{{Title: "Hello", Artist: "Adele", Album: "21"}}, 500, 1)
164+ m := newModel([]Song{{Title: "Hello", Artist: "Adele", Album: "21"}}, 1)
165 m.titleInput.SetValue(tt.title)
166 m.artistInput.SetValue(tt.artist)
167 m.albumInput.SetValue(tt.album)
168diff --git a/ui.go b/ui.go
169index 29f40626228e5f914dcb464db205d23bd11ad21e..0c9f79a9e3fb5acf3a30b63dccdebc968b7dd8f2 100644
170--- a/ui.go
171+++ b/ui.go
172@@ -29,12 +29,11 @@ type model struct {
173 played int
174 score int
175 correct int
176- threshold int
177
178 titleInput textinput.Model
179 artistInput textinput.Model
180 albumInput textinput.Model
181- active int // 0 = title, 1 = artist, 2 = album
182+ active int // 0 = title, 1 = album, 2 = artist
183
184 st state
185 playing bool
186@@ -44,7 +43,7 @@ type model struct {
187 done bool
188 }
189
190-func newModel(catalog []Song, threshold, rounds int) model {
191+func newModel(catalog []Song, rounds int) model {
192 ti := textinput.New()
193 ti.Prompt = "Title: "
194 tt := textinput.New()
195@@ -55,7 +54,6 @@ func newModel(catalog []Song, threshold, rounds int) model {
196 m := model{
197 catalog: catalog,
198 rounds: rounds,
199- threshold: threshold,
200 titleInput: ti,
201 artistInput: tt,
202 albumInput: ta,
203@@ -73,15 +71,15 @@ func newModel(catalog []Song, threshold, rounds int) model {
204 }
205
206 func (m model) Init() tea.Cmd {
207- return startAudio(m.answer, m.threshold, 1)
208+ return startAudio(m.answer, 1)
209 }
210
211 // startAudio is a command that plays the one-second segment of song and then
212 // reports back with audioDoneMsg. bubbletea runs it in a goroutine, so the TUI
213 // stays responsive while the audio plays.
214-func startAudio(song Song, threshold, seconds int) tea.Cmd {
215+func startAudio(song Song, seconds int) tea.Cmd {
216 return func() tea.Msg {
217- _ = playSegment(song.Path, threshold, seconds)
218+ _ = playSegment(song.Path, seconds)
219 return audioDoneMsg{}
220 }
221 }
222@@ -117,14 +115,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
223 return m, nil
224 }
225 m.playing = true
226- return m, startAudio(m.answer, m.threshold, 1)
227+ return m, startAudio(m.answer, 1)
228 case tea.KeyCtrlL:
229 if m.playing {
230 return m, nil
231 }
232 m.playing = true
233 m.hinted = true
234- return m, startAudio(m.answer, m.threshold, 2)
235+ return m, startAudio(m.answer, 2)
236 case tea.KeyEnter:
237 if m.playing {
238 return m, nil
239@@ -135,9 +133,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
240 case 0:
241 m.titleInput, _ = m.titleInput.Update(msg)
242 case 1:
243- m.artistInput, _ = m.artistInput.Update(msg)
244- default:
245 m.albumInput, _ = m.albumInput.Update(msg)
246+ default:
247+ m.artistInput, _ = m.artistInput.Update(msg)
248 }
249 return m, nil
250 }
251@@ -180,21 +178,21 @@ func (m *model) beginRound(song Song) tea.Cmd {
252 m.titleInput.Focus()
253 m.artistInput.Blur()
254 m.albumInput.Blur()
255- return startAudio(song, m.threshold, 1)
256+ return startAudio(song, 1)
257 }
258
259 func (m *model) toggleField() {
260 m.active = (m.active + 1) % 3
261- for _, in := range []textinput.Model{m.titleInput, m.artistInput, m.albumInput} {
262- in.Blur()
263- }
264+ m.titleInput.Blur()
265+ m.albumInput.Blur()
266+ m.artistInput.Blur()
267 switch m.active {
268 case 0:
269 m.titleInput.Focus()
270 case 1:
271- m.artistInput.Focus()
272diff --git a/ui_test.go b/ui_test.go
273index 48903bce22360db7cbe1b64148963bae83092542..93aa6092c3c7703a5172c15cdd853806ecbc6203 100644
274--- a/ui_test.go
275+++ b/ui_test.go
276@@ -7,7 +7,7 @@ import (
277 )
278
279 func TestReplay(t *testing.T) {
280- m := newModel([]Song{{Title: "Song", Artist: "Artist", Path: "song.mp3"}}, 500, 1)
281+ m := newModel([]Song{{Title: "Song", Artist: "Artist", Path: "song.mp3"}}, 1)
282 updated, _ := m.Update(audioDoneMsg{})
283 m = updated.(model)
284 m.titleInput.SetValue("my title")
285@@ -39,7 +39,19 @@ func TestReplay(t *testing.T) {
286
287 updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyTab})
288 m = updated.(model)
289- if m.active != 1 || !m.artistInput.Focused() {
290- t.Fatal("Tab should switch to the artist field")
291+ if m.active != 1 || !m.albumInput.Focused() {
292+ t.Fatal("Tab should switch to the album field")
293+ }
294+
295+ updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyTab})
296+ m = updated.(model)
297+ if m.active != 2 || !m.artistInput.Focused() {
298+ t.Fatal("second Tab should switch to the artist field")
299+ }
300+
301+ updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyTab})
302+ m = updated.(model)
303+ if m.active != 0 || !m.titleInput.Focused() {
304+ t.Fatal("third Tab should wrap back to the title field")
305 }
306 }