a31c34604286fcaa994935944a913ef9afca6ead

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Add in-memory audio generation and /audio endpoint

Extract generateSegment from playSegment so a round's clip is produced as
WAV bytes. The TUI path (playSegment) now pipes those bytes to aplay via
stdin instead of a temp file. Game.beginRound generates the segment for the
current song, and a new /audio handler serves it with no-store caching.

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+}