Diff
1diff --git a/audio.go b/audio.go
2index 1c7858fd02f0c030669f8a44415198eab99b8d99..3387dfef98e593dae791a56f48f0fc67ed0f23ee 100644
3--- a/audio.go
4+++ b/audio.go
5@@ -8,7 +8,6 @@ import (
6 "math/rand"
7 "os"
8 "os/exec"
9- "path/filepath"
10 "strconv"
11 "strings"
12 )
13@@ -125,24 +124,30 @@ func pickStart(songPath string) (int, error) {
14 return offset + start/sampleRate, nil
15 }
16
17-// playSegment encodes `seconds` of the song starting at startSec into a temp
18-// WAV and plays it via aplay.
19-func playSegment(songPath string, startSec, seconds int) error {
20- dir, err := os.MkdirTemp("", "museguessr-*")
21- if err != nil {
22- return err
23- }
24- defer func() { _ = os.RemoveAll(dir) }()
25-
26- wavPath := filepath.Join(dir, "segment.wav")
27+// generateSegment decodes `seconds` of audio starting at startSec into an
28+// in-memory mono WAV buffer at sampleRate Hz. The bytes can be played locally
29+// with aplay (TUI) or served over HTTP (web UI).
30+func generateSegment(songPath string, startSec, seconds int) ([]byte, error) {
31 cmd := exec.Command("ffmpeg", "-nostdin", "-loglevel", "error",
32 "-ss", strconv.Itoa(startSec), "-t", strconv.Itoa(seconds),
33- "-i", songPath, "-ac", "1", "-ar", strconv.Itoa(sampleRate), wavPath)
34+ "-i", songPath, "-ac", "1", "-ar", strconv.Itoa(sampleRate),
35+ "-f", "wav", "-")
36+ var out bytes.Buffer
37+ cmd.Stdout = &out
38 if err := cmd.Run(); err != nil {
39- return fmt.Errorf("ffmpeg encode: %w", err)
40+ return nil, fmt.Errorf("ffmpeg encode: %w", err)
41 }
42+ return out.Bytes(), nil
43+}
44
45- cmd = exec.Command("aplay", "-q", wavPath)
46+// playSegment decodes a segment and plays it with aplay (used by the TUI).
47+func playSegment(songPath string, startSec, seconds int) error {
48+ wav, err := generateSegment(songPath, startSec, seconds)
49+ if err != nil {
50+ return err
51+ }
52+ cmd := exec.Command("aplay", "-q")
53+ cmd.Stdin = bytes.NewReader(wav)
54 cmd.Stdout = os.Stderr
55 cmd.Stderr = os.Stderr
56 return cmd.Run()
57diff --git a/server.go b/server.go
58index 6ae9a8fa90b243d4d22cbce699ec1973b85699c1..174a7d0810896fa2b46cf91ed261f46ee577d675 100644
59--- a/server.go
60+++ b/server.go
61@@ -5,6 +5,7 @@ import (
62 "html/template"
63 "io/fs"
64 "net/http"
65+ "strconv"
66 "strings"
67 "sync"
68 )
69@@ -62,6 +63,16 @@ func (g *Game) beginRound() {
70 g.result = nil
71 g.startSec = -1
72 g.segment = nil
73+ start, err := pickStart(g.answer.Path)
74+ if err != nil {
75+ return
76+ }
77+ g.startSec = start
78+ wav, werr := generateSegment(g.answer.Path, start, defaultPlay)
79+ if werr != nil {
80+ return
81+ }
82+ g.segment = wav
83 }
84
85 func (g *Game) nextSong() Song {
86@@ -117,6 +128,7 @@ func (s *server) handler() http.Handler {
87 mux := http.NewServeMux()
88 staticSub, _ := fs.Sub(staticFS, "web/static")
89 mux.HandleFunc("/", s.handleHome)
90+ mux.HandleFunc("/audio", s.handleAudio)
91 mux.Handle("/static/", stripPrefix("/static/", http.FileServer(http.FS(staticSub))))
92 return mux
93 }
94@@ -144,3 +156,19 @@ func (s *server) handleHome(w http.ResponseWriter, r *http.Request) {
95 http.Error(w, err.Error(), http.StatusInternalServerError)
96 }
97 }
98+
99+func (s *server) handleAudio(w http.ResponseWriter, r *http.Request) {
100+ s.game.mu.Lock()
101+ seg := s.game.segment
102+ s.game.mu.Unlock()
103+ if len(seg) == 0 {
104+ http.NotFound(w, r)
105+ return
106+ }
107+ w.Header().Set("Content-Type", "audio/wav")
108+ w.Header().Set("Cache-Control", "no-store")
109+ w.Header().Set("Content-Length", strconv.Itoa(len(seg)))
110+ if _, err := w.Write(seg); err != nil {
111+ return
112+ }
113+}