diff --git a/serve.go b/serve.go index 8672db2..5cdb4b5 100644 --- a/serve.go +++ b/serve.go @@ -54,10 +54,20 @@ func cmdServe(args []string) { mux.HandleFunc("/api/search", handleSearch) mux.HandleFunc("/api/list", handleList) + mux.HandleFunc("/api/list-wavs", handleListWavs) mux.HandleFunc("/api/download", handleAPIDownload) mux.HandleFunc("/api/extract", handleAPIExtract) mux.HandleFunc("/api/progress", handleProgress) + wavsBase := "./output/wavs" + if abs, err := filepath.Abs(wavsBase); err == nil { + wavsBase = abs + } + if err := os.MkdirAll(wavsBase, 0755); err != nil { + fatal("create wavs dir: %v", err) + } + mux.Handle("/wavs/", http.StripPrefix("/wavs/", http.FileServer(http.Dir(wavsBase)))) + addr := fmt.Sprintf("%s:%d", host, port) listener, err := net.Listen("tcp", addr) if err != nil { @@ -254,6 +264,30 @@ func handleAPIDownload(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]any{"jobs": jobIDs}) } +func handleListWavs(w http.ResponseWriter, r *http.Request) { + dir := r.URL.Query().Get("dir") + if dir == "" { + dir = "./output/wavs" + } + if abs, err := filepath.Abs(dir); err == nil { + dir = abs + } + + entries, err := os.ReadDir(dir) + if err != nil { + writeJSON(w, map[string]any{"error": err.Error()}) + return + } + + var files []string + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(strings.ToLower(e.Name()), ".wav") { + files = append(files, e.Name()) + } + } + writeJSON(w, files) +} + func handleProgress(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") diff --git a/serve_test.go b/serve_test.go index eed25e2..20e3ffc 100644 --- a/serve_test.go +++ b/serve_test.go @@ -442,4 +442,68 @@ func TestHandleAPIExtract_ExtractISOError(t *testing.T) { if !strings.Contains(msg, "FAIL bad.iso") { t.Errorf("expected FAIL message, got: %s", msg) } +} + +func TestHandleListWavs_ReturnsWavFiles(t *testing.T) { + tmp := t.TempDir() + wavDir := filepath.Join(tmp, "wavs") + if err := os.MkdirAll(wavDir, 0755); err != nil { + t.Fatal(err) + } + for _, name := range []string{"kick.wav", "snare.wav", "hihat.wav"} { + fd, _ := os.Create(filepath.Join(wavDir, name)) + _ = fd.Close() + } + fd, _ := os.Create(filepath.Join(wavDir, "readme.txt")) + _ = fd.Close() + + req := httptest.NewRequest("GET", "/api/list-wavs?dir="+wavDir, nil) + rr := httptest.NewRecorder() + handleListWavs(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + var files []string + if err := json.Unmarshal(rr.Body.Bytes(), &files); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if len(files) != 3 { + t.Errorf("expected 3 wav files, got %d: %v", len(files), files) + } +} + +func TestHandleListWavs_EmptyDir(t *testing.T) { + tmp := t.TempDir() + req := httptest.NewRequest("GET", "/api/list-wavs?dir="+tmp, nil) + rr := httptest.NewRecorder() + handleListWavs(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + var files []string + if err := json.Unmarshal(rr.Body.Bytes(), &files); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if len(files) != 0 { + t.Errorf("expected 0 files, got %d", len(files)) + } +} + +func TestHandleListWavs_NonExistentDir(t *testing.T) { + req := httptest.NewRequest("GET", "/api/list-wavs?dir=/nonexistent/wavs", nil) + rr := httptest.NewRecorder() + handleListWavs(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + var resp map[string]any + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if _, ok := resp["error"]; !ok { + t.Error("expected error for nonexistent dir") + } } \ No newline at end of file diff --git a/util_test.go b/util_test.go index 44a36f1..c17d8a2 100644 --- a/util_test.go +++ b/util_test.go @@ -99,6 +99,8 @@ func TestFindAkaiutil(t *testing.T) { }() t.Run("env var set to missing file", func(t *testing.T) { + tmp := t.TempDir() + t.Chdir(tmp) _ = os.Setenv("AKAIUTIL", "/nonexistent/akaiutil") os.Args[0] = "/some/binary" _, err := findAkaiutil() @@ -109,6 +111,7 @@ func TestFindAkaiutil(t *testing.T) { t.Run("env var set to existing file", func(t *testing.T) { tmp := t.TempDir() + t.Chdir(tmp) f := tmp + "/myutil" fd, _ := os.Create(f) defer func() { _ = fd.Close() }() @@ -124,6 +127,7 @@ func TestFindAkaiutil(t *testing.T) { t.Run("finds binary next to argv[0]", func(t *testing.T) { tmp := t.TempDir() + t.Chdir(tmp) exe := tmp + "/mybinary" fd, _ := os.Create(exe) defer func() { _ = fd.Close() }() @@ -143,6 +147,7 @@ func TestFindAkaiutil(t *testing.T) { t.Run("finds binary in third_party relative to argv[0]", func(t *testing.T) { tmp := t.TempDir() + t.Chdir(tmp) exe := tmp + "/somebin" fd, _ := os.Create(exe) defer func() { _ = fd.Close() }() @@ -166,6 +171,7 @@ func TestFindAkaiutil(t *testing.T) { t.Run("env var takes priority over path candidates", func(t *testing.T) { tmp := t.TempDir() + t.Chdir(tmp) envFile := tmp + "/envutil" pathFile := tmp + "/pathutil" fd1, _ := os.Create(envFile) @@ -185,6 +191,7 @@ func TestFindAkaiutil(t *testing.T) { t.Run("no akaiutil found returns error", func(t *testing.T) { tmp := t.TempDir() + t.Chdir(tmp) exe := tmp + "/nobinaryhere" fd, _ := os.Create(exe) defer func() { _ = fd.Close() }() diff --git a/web/ui/app.js b/web/ui/app.js index b5f92e9..7f7e980 100644 --- a/web/ui/app.js +++ b/web/ui/app.js @@ -183,6 +183,129 @@ async function doExtract() { } } +// ─── WAV Browser ────────────────────────────────────────────────── + +let audioCtx = null; +let currentSource = null; +let currentAudioBuffer = null; +let startTime = 0; +let pauseOffset = 0; +let isPlaying = false; +let animationFrame = null; + +document.getElementById('wav-browse-btn').addEventListener('click', browseWavs); + +async function browseWavs() { + const dir = document.getElementById('wav-dir').value; + const list = document.getElementById('wav-list'); + list.innerHTML = 'Loading...'; + + try { + const resp = await fetch(`${API}/list-wavs?dir=${encodeURIComponent(dir)}`); + const data = await resp.json(); + if (data.error) { + list.innerHTML = 'Error: ' + data.error + ''; + return; + } + if (!Array.isArray(data) || data.length === 0) { + list.innerHTML = 'No WAV files found'; + return; + } + list.innerHTML = data.map(f => ` +