File size: 2,944 Bytes
b2d4d0e
 
 
ded73f2
b2d4d0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ded73f2
 
b2d4d0e
 
ded73f2
b2d4d0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65949d0
b2d4d0e
 
65949d0
b2d4d0e
 
65949d0
b2d4d0e
 
 
 
 
65949d0
b2d4d0e
65949d0
b2d4d0e
 
 
 
 
 
65949d0
b2d4d0e
65949d0
b2d4d0e
 
 
 
 
 
65949d0
b2d4d0e
65949d0
b2d4d0e
 
 
 
65949d0
 
 
b2d4d0e
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import { useRef, useState } from "react";
import { Recorder } from "@/lib/audio";
import { addVoice } from "@/lib/idb";
import { encodeWav } from "@/lib/wav";

type Props = {
  onSaved: () => void;
};

export default function VoiceComposer({ onSaved }: Props) {
  const fileRef = useRef<HTMLInputElement>(null);
  const recorderRef = useRef<Recorder | null>(null);
  const [recState, setRecState] = useState<"idle" | "recording" | "stopping" | "error">("idle");
  const [name, setName] = useState("");

  async function importBlob(blob: Blob, defaultName: string) {
    const arr = new Uint8Array(await blob.arrayBuffer());
    const ctx = new AudioContext();
    const buf = await ctx.decodeAudioData(arr.buffer.slice(0));
    // Re-encode as 16-bit PCM mono WAV so the server (libsndfile) can decode it.
    const wav = encodeWav(buf);
    await addVoice({
      name: name || defaultName || `voice-${Date.now()}`,
      blob: wav,
      sampleRate: buf.sampleRate,
      durationMs: Math.round(buf.duration * 1000),
    });
    setName("");
    onSaved();
  }

  async function onFile(e: React.ChangeEvent<HTMLInputElement>) {
    const f = e.target.files?.[0];
    if (!f) return;
    await importBlob(f, f.name.replace(/\.[^.]+$/, ""));
    e.target.value = "";
  }

  async function startRec() {
    const r = new Recorder();
    recorderRef.current = r;
    try {
      await r.start();
      setRecState("recording");
    } catch {
      setRecState("error");
    }
  }

  async function stopRec() {
    setRecState("stopping");
    const blob = await recorderRef.current?.stop();
    setRecState("idle");
    if (blob) await importBlob(blob, "recorded");
  }

  return (
    <div className="space-y-3">
      <input
        type="text"
        placeholder="Name this voice"
        value={name}
        onChange={(e) => setName(e.target.value)}
        className="field-input"
      />
      <div className="flex gap-2">
        <button
          type="button"
          onClick={() => fileRef.current?.click()}
          className="btn-ghost flex-1"
        >
          ↑ Upload
        </button>
        <input ref={fileRef} type="file" accept="audio/*" hidden onChange={onFile} />
        {recState === "recording" ? (
          <button
            type="button"
            onClick={stopRec}
            className="btn-primary flex-1 !py-2 flex items-center justify-center gap-2"
          >
            <span className="size-1.5 rounded-full bg-current animate-pulse-dot" />
            Stop &amp; save
          </button>
        ) : (
          <button
            type="button"
            onClick={startRec}
            className="btn-ghost flex-1"
          >
            ● Record
          </button>
        )}
      </div>
      {recState === "error" && (
        <p className="text-[11px] text-red-400 font-mono uppercase tracking-wider">
          microphone permission denied
        </p>
      )}
    </div>
  );
}