webtools

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

page.tsx (11800B)


      1 "use client";
      2 
      3 import { useEffect, useRef, useState } from "react";
      4 
      5 export default function AudioAnalysisPage() {
      6   type InputType = "file" | "mic" | "rec";
      7   const [inputType, setInputType] = useState<InputType>("file");
      8   const [isAnalyzing, setIsAnalyzing] = useState(false);
      9   const [pitchResult, setPitchResult] = useState<{
     10     frequency: number;
     11     period: number;
     12   } | null>(null);
     13 
     14   const audioContextRef = useRef<AudioContext | null>(null);
     15   const analyserRef = useRef<AnalyserNode | null>(null);
     16   const oscCanvasRef = useRef<HTMLCanvasElement>(null);
     17   const specCanvasRef = useRef<HTMLCanvasElement>(null);
     18   const singlePeriodCanvasRef = useRef<HTMLCanvasElement>(null);
     19   const fileInputRef = useRef<HTMLInputElement>(null);
     20 
     21   const updateInputType = (event: React.ChangeEvent<HTMLSelectElement>) => {
     22     setIsAnalyzing(false);
     23     setPitchResult(null);
     24     setInputType(event.target.value as InputType);
     25   };
     26 
     27   const performPitchAnalysis = () => {
     28     const analyser = analyserRef.current;
     29     const context = audioContextRef.current;
     30     if (!analyser || !context) return;
     31 
     32     const timeDomainBuffer = new Float32Array(analyser.fftSize);
     33     analyser.getFloatTimeDomainData(timeDomainBuffer);
     34 
     35     const sampleRate = context.sampleRate;
     36     let bestOffset = -1;
     37     let maxCorrelation = 0;
     38     const minPeriodSamples = sampleRate / 1000;
     39     const maxPeriodSamples = sampleRate / 50;
     40 
     41     for (
     42       let offset = Math.floor(minPeriodSamples);
     43       offset < maxPeriodSamples;
     44       offset++
     45     ) {
     46       let correlation = 0;
     47       for (let i = 0; i < timeDomainBuffer.length - offset; i++) {
     48         correlation += timeDomainBuffer[i] * timeDomainBuffer[i + offset];
     49       }
     50       if (correlation > maxCorrelation) {
     51         maxCorrelation = correlation;
     52         bestOffset = offset;
     53       }
     54     }
     55 
     56     if (bestOffset > 0) {
     57       const detectedFrequency = sampleRate / bestOffset;
     58       setPitchResult({ frequency: detectedFrequency, period: bestOffset });
     59 
     60       const canvas = singlePeriodCanvasRef.current;
     61       if (canvas) {
     62         const ctx = canvas.getContext("2d");
     63         if (ctx) {
     64           ctx.fillStyle = "#141414";
     65           ctx.fillRect(0, 0, canvas.width, canvas.height);
     66           ctx.strokeStyle = "#00ffff";
     67           ctx.lineWidth = 3;
     68           ctx.beginPath();
     69 
     70           let startSampleIndex = 0;
     71           for (let i = 1; i < timeDomainBuffer.length / 2; i++) {
     72             if (timeDomainBuffer[i - 1] < 0 && timeDomainBuffer[i] >= 0) {
     73               startSampleIndex = i;
     74               break;
     75             }
     76           }
     77 
     78           const xStep = canvas.width / bestOffset;
     79           for (let i = 0; i < bestOffset; i++) {
     80             const sampleValue = timeDomainBuffer[startSampleIndex + i] || 0;
     81             const x = i * xStep;
     82             const y = (1 - sampleValue) * (canvas.height / 2);
     83             i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
     84           }
     85           ctx.stroke();
     86         }
     87       }
     88     }
     89   };
     90 
     91   useEffect(() => {
     92     if (!isAnalyzing) return;
     93 
     94     const canvases = [oscCanvasRef.current, specCanvasRef.current];
     95     canvases.forEach((canvas) => {
     96       if (canvas) {
     97         const ctx = canvas.getContext("2d");
     98         if (ctx) {
     99           ctx.fillStyle = "#141414";
    100           ctx.fillRect(0, 0, canvas.width, canvas.height);
    101         }
    102       }
    103     });
    104 
    105     let animationFrameId: number;
    106     let audioSourceNode: AudioNode | null = null;
    107     let mediaStream: MediaStream | null = null;
    108     let audioUrl: string | null = null;
    109     const internalAudioElement = new Audio();
    110 
    111     const startAudioSystem = async () => {
    112       if (!audioContextRef.current) {
    113         const AudioContextClass =
    114           window.AudioContext || (window as any).webkitAudioContext;
    115         audioContextRef.current = new AudioContextClass();
    116       }
    117       const context = audioContextRef.current;
    118       if (context.state === "suspended") await context.resume();
    119 
    120       const analyser = context.createAnalyser();
    121       analyser.fftSize = 4096;
    122       analyserRef.current = analyser;
    123 
    124       if (inputType === "file" && fileInputRef.current?.files?.[0]) {
    125         audioUrl = URL.createObjectURL(fileInputRef.current.files[0]);
    126         internalAudioElement.src = audioUrl;
    127         internalAudioElement.onended = () => setIsAnalyzing(false);
    128         internalAudioElement.play();
    129         audioSourceNode =
    130           context.createMediaElementSource(internalAudioElement);
    131         audioSourceNode.connect(analyser);
    132         analyser.connect(context.destination);
    133       } else if (inputType === "mic") {
    134         try {
    135           mediaStream = await navigator.mediaDevices.getUserMedia({
    136             audio: true,
    137           });
    138           audioSourceNode = context.createMediaStreamSource(mediaStream);
    139           audioSourceNode.connect(analyser);
    140         } catch (error) {
    141           console.error(error);
    142           setIsAnalyzing(false);
    143           return;
    144         }
    145       } else {
    146         setIsAnalyzing(false);
    147         return;
    148       }
    149 
    150       const drawLoop = () => {
    151         const oscCanvas = oscCanvasRef.current;
    152         const specCanvas = specCanvasRef.current;
    153 
    154         if (oscCanvas) {
    155           const ctx = oscCanvas.getContext("2d");
    156           if (ctx) {
    157             const timeData = new Uint8Array(analyser.frequencyBinCount);
    158             analyser.getByteTimeDomainData(timeData);
    159             ctx.fillStyle = "#141414";
    160             ctx.fillRect(0, 0, oscCanvas.width, oscCanvas.height);
    161             ctx.lineWidth = 2;
    162             ctx.strokeStyle = "#00ff00";
    163             ctx.beginPath();
    164             const sliceWidth = oscCanvas.width / timeData.length;
    165             let x = 0;
    166             for (let i = 0; i < timeData.length; i++) {
    167               const y = (timeData[i] / 128.0) * (oscCanvas.height / 2);
    168               i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
    169               x += sliceWidth;
    170             }
    171             ctx.lineTo(oscCanvas.width, oscCanvas.height / 2);
    172             ctx.stroke();
    173           }
    174         }
    175 
    176         if (specCanvas) {
    177           const ctx = specCanvas.getContext("2d", { willReadFrequently: true });
    178           if (ctx) {
    179             const freqData = new Uint8Array(analyser.frequencyBinCount);
    180             analyser.getByteFrequencyData(freqData);
    181             ctx.drawImage(
    182               specCanvas,
    183               0,
    184               0,
    185               specCanvas.width,
    186               specCanvas.height - 1,
    187               0,
    188               1,
    189               specCanvas.width,
    190               specCanvas.height - 1,
    191             );
    192 
    193             const currentSampleRate = context.sampleRate;
    194             const nyquistFrequency = currentSampleRate / 2;
    195             const displayMaxFrequency = 8000;
    196             const binsToDisplay = Math.floor(
    197               (displayMaxFrequency / nyquistFrequency) * freqData.length,
    198             );
    199 
    200             const barWidth = specCanvas.width / binsToDisplay;
    201             for (let i = 0; i < binsToDisplay; i++) {
    202               const value = freqData[i];
    203               const hue = 240 - (value / 255) * 240;
    204               ctx.fillStyle = value > 0 ? `hsl(${hue}, 100%, 50%)` : "black";
    205               ctx.fillRect(i * barWidth, 0, barWidth + 1, 1);
    206             }
    207 
    208             ctx.fillStyle = "rgba(255, 255, 255, 0.6)";
    209             for (
    210               let frequency = 440;
    211               frequency < displayMaxFrequency;
    212               frequency += 440
    213             ) {
    214               const xPosition =
    215                 (frequency / displayMaxFrequency) * specCanvas.width;
    216               ctx.fillRect(xPosition, 0, 1, 1);
    217             }
    218           }
    219         }
    220         animationFrameId = requestAnimationFrame(drawLoop);
    221       };
    222 
    223       animationFrameId = requestAnimationFrame(drawLoop);
    224     };
    225 
    226     startAudioSystem();
    227 
    228     return () => {
    229       cancelAnimationFrame(animationFrameId);
    230       audioSourceNode?.disconnect();
    231       mediaStream?.getTracks().forEach((track) => track.stop());
    232       internalAudioElement.pause();
    233       internalAudioElement.src = "";
    234       if (audioUrl) URL.revokeObjectURL(audioUrl);
    235       analyserRef.current = null;
    236     };
    237   }, [isAnalyzing, inputType]);
    238 
    239   useEffect(() => {
    240     return () => {
    241       if (audioContextRef.current) audioContextRef.current.close();
    242     };
    243   }, []);
    244 
    245   return (
    246     <main style={{ padding: "20px", maxWidth: "900px", margin: "0 auto" }}>
    247       <h1>Audio Analyser</h1>
    248       <section
    249         style={{
    250           marginBottom: "20px",
    251           background: "#f5f5f5",
    252           padding: "15px",
    253           borderRadius: "8px",
    254         }}
    255       >
    256         <h2>Select Input</h2>
    257         <div
    258           style={{
    259             display: "flex",
    260             alignItems: "center",
    261             gap: "10px",
    262             flexWrap: "wrap",
    263           }}
    264         >
    265           <select
    266             onChange={updateInputType}
    267             value={inputType}
    268             style={{ padding: "5px" }}
    269           >
    270             <option value="file">File</option>
    271             <option value="mic">Microphone</option>
    272             <option value="rec">Record (Not Implemented)</option>
    273           </select>
    274           {inputType === "file" && (
    275             <input type="file" ref={fileInputRef} accept="audio/*" />
    276           )}
    277           <div style={{ marginLeft: "auto", display: "flex", gap: "10px" }}>
    278             <button
    279               onClick={() => setIsAnalyzing(!isAnalyzing)}
    280               style={{
    281                 padding: "8px 16px",
    282                 backgroundColor: isAnalyzing ? "#ff0000" : "#0070f3",
    283                 color: "white",
    284                 border: "none",
    285                 borderRadius: "4px",
    286                 cursor: "pointer",
    287               }}
    288             >
    289               {isAnalyzing ? "Stop Analysis" : "Start Analysis"}
    290             </button>
    291             {isAnalyzing && (
    292               <button
    293                 onClick={performPitchAnalysis}
    294                 style={{
    295                   padding: "8px 16px",
    296                   backgroundColor: "#28a745",
    297                   color: "white",
    298                   border: "none",
    299                   borderRadius: "4px",
    300                   cursor: "pointer",
    301                 }}
    302               >
    303                 Analyze Pitch & Single Period
    304               </button>
    305             )}
    306           </div>
    307         </div>
    308       </section>
    309 
    310       {pitchResult && (
    311         <article
    312           style={{
    313             marginBottom: "30px",
    314             padding: "15px",
    315             border: "2px solid #28a745",
    316             borderRadius: "8px",
    317           }}
    318         >
    319           <h2>Pitch Analysis Result</h2>
    320           <p style={{ fontSize: "1.2rem", fontWeight: "bold" }}>
    321             Detected Frequency:{" "}
    322             <span style={{ color: "#28a745" }}>
    323               {pitchResult.frequency.toFixed(2)} Hz
    324             </span>
    325           </p>
    326           <h3>Single Period Oscilloscope</h3>
    327           <canvas
    328             ref={singlePeriodCanvasRef}
    329             width="800"
    330             height="150"
    331             style={{
    332               border: "1px solid #333",
    333               background: "#141414",
    334               width: "100%",
    335               height: "auto",
    336             }}
    337           />
    338         </article>
    339       )}
    340 
    341       <article style={{ marginBottom: "20px" }}>
    342         <h2>Oscilloscope (Real-time)</h2>
    343         <canvas
    344           ref={oscCanvasRef}
    345           width="800"
    346           height="200"
    347           style={{
    348             border: "1px solid #333",
    349             background: "#141414",
    350             width: "100%",
    351             height: "auto",
    352           }}
    353         />
    354       </article>
    355 
    356       <article>
    357         <h2>Spectrogram (Real-time)</h2>
    358         <canvas
    359           ref={specCanvasRef}
    360           width="800"
    361           height="400"
    362           style={{
    363             border: "1px solid #333",
    364             background: "#141414",
    365             width: "100%",
    366             height: "auto",
    367           }}
    368         />
    369       </article>
    370     </main>
    371   );
    372 }