From 1caebf3d8a8ba9deb7fb77fcc17b02c46652f9f1 Mon Sep 17 00:00:00 2001
From: Pinapelz <yukais@pinapelz.com>
Date: Thu, 16 Nov 2023 21:49:11 -0800
Subject: feat: add supplementary audio upload option

---
 src/app/page.tsx | 240 +++++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 190 insertions(+), 50 deletions(-)

(limited to 'src/app/page.tsx')

diff --git a/src/app/page.tsx b/src/app/page.tsx
index 4be028c..457610d 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,4 +1,4 @@
-"use client"
+"use client";
 import React, { useEffect, useRef, useState } from "react";
 import styled from "styled-components";
 import KaraokePlayer from "./components/KaraokePlayer";
@@ -35,6 +35,7 @@ const FileInput = styled.input`
   padding: 10px 15px;
   border-radius: 5px;
   border: 1px solid #ddd;
+  justify-content: center;
   cursor: pointer;
   display: none;
   font-family: Arial;
@@ -57,29 +58,46 @@ const FileInputLabel = styled.label`
   }
 `;
 
+const ControlBarButton = styled.button`
+  padding: 10px 15px;
+  border-radius: 5px;
+  border: 1px solid #ddd;
+  align-items: center;
+  cursor: pointer;
+  &:hover,
+  &:focus {
+    background-color: #eaeaea;
+    outline: none;
+  }
+`;
+
 const StyledLink = styled.a`
-    font-size: 20px;
-    font-family: Arial;
-    text-decoration: none;
-    text-color: black;
-    &:hover {
-        text-decoration: underline;
-    }
-    `;
+  font-size: 20px;
+  font-family: Arial;
+  text-decoration: none;
+  text-color: black;
+  &:hover {
+    text-decoration: underline;
+  }
+`;
 
 function KaraokePage() {
   const [currentMillisecond, setCurrentMillisecond] = useState(0);
-  const [lrcContent, setLrcContent] = useState("");
-  const [videoUrl, setVideoUrl] = useState("");
-  const [isPlaying, setIsPlaying] = useState(false);
-  const [showVolume, setShowVolume] = useState(false);
-  const [scrubValue, setScrubValue] = useState(0);
-  const [showFileInputs, setShowFileInputs] = useState(true);
+  const [lrcContent, setLrcContent] = useState<string>("");
+  const [videoUrl, setVideoUrl] = useState<string>("");
+  const [supplementAudioUrl, setSupplementAudioUrl] = useState<string>("");
+  const [isPlaying, setIsPlaying] = useState<boolean>(false);
+  const [showVolume, setShowVolume] = useState<boolean>(false);
+  const [scrubValue, setScrubValue] = useState<number>(0);
+  const [showFileInputs, setShowFileInputs] = useState<boolean>(true);
   const videoRef = useRef<HTMLVideoElement>(null);
-  const [captionsText, setCaptionsText] = useState("");
-  const [offset, setOffset] = useState("0");
-  const [dragOver, setDragOver] = useState(false);
-  const [statusText, setStatusText] = useState("No video selected");
+  const supplementAudioRef = useRef<HTMLAudioElement>(null);
+  const [captionsText, setCaptionsText] = useState<string>("");
+  const [offset, setOffset] = useState<string>("0");
+  const [dragOver, setDragOver] = useState<boolean>(false);
+  const [statusText, setStatusText] = useState<string>("No video selected");
+  const [balance, setBalance] = useState<number>(0);
+  const [supplementAudioOffset, setSupplementAudioOffset] = useState<string>("0");
 
   // Functions for handling file input changes
   const handleLrcFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -118,7 +136,26 @@ function KaraokePage() {
       };
       reader.readAsText(file);
       toast.success("SRV file loaded successfully", { autoClose: 2000 });
-  }
+    }
+  };
+
+  const handleSupplementAudioFileChange = (
+    event: React.ChangeEvent<HTMLInputElement>
+  ) => {
+    const file = event.target.files?.[0];
+    const video = videoRef.current;
+    if (file) {
+      const url = URL.createObjectURL(file);
+      setSupplementAudioUrl(url);
+      setCurrentMillisecond(0);
+      setScrubValue(0);
+      setIsPlaying(false);
+      if (video) 
+        video.pause();
+      toast.success("Supplemental Audio file loaded successfully", {
+        autoClose: 2000,
+      });
+    }
   };
 
   // Side effects for keyboard shortcuts
@@ -161,6 +198,26 @@ function KaraokePage() {
     };
   });
 
+  useEffect(() => {
+    const video = videoRef.current;
+    const audio = supplementAudioRef.current;
+    if (!video || !audio) return;
+  
+    if (balance < 0) {
+      video.volume = (1 + balance); 
+    } else {
+      video.volume = 1;
+      audio.volume = (1 - balance);
+    }
+  }, [balance]);
+
+  useEffect(() => {
+    const video = videoRef.current;
+    const audio = supplementAudioRef.current;
+    if (!video || !audio) return;
+    if (supplementAudioOffset === "" || supplementAudioOffset == null) return;
+    audio.currentTime = video.currentTime + parseInt(supplementAudioOffset)/1000;
+  },[supplementAudioOffset]);
 
   // General video control functionality
 
@@ -174,13 +231,16 @@ function KaraokePage() {
 
     if (video.paused) {
       video.play();
+      if (supplementAudioUrl) supplementAudioRef.current?.play();
       setIsPlaying(true);
     } else {
       video.pause();
+      if (supplementAudioUrl) supplementAudioRef.current?.pause();
       setIsPlaying(false);
     }
   };
 
+  // Status text styling depending on whats loaded. Not all visible
   useEffect(() => {
     if (videoUrl && lrcContent) {
       setStatusText("Ready to play!");
@@ -193,19 +253,47 @@ function KaraokePage() {
     }
   }, [videoUrl, lrcContent]);
 
+  //  Video Control Bar functionality
   const handleScrub = (event: React.ChangeEvent<HTMLInputElement>) => {
     const time =
       (parseFloat(event.target.value) / 100) * videoRef.current!.duration;
     videoRef.current!.currentTime = time;
+    if (supplementAudioOffset === "" || supplementAudioOffset == null){
+      supplementAudioRef.current!.currentTime = time;
+    }
+    else {
+      supplementAudioRef.current!.currentTime = time + parseInt(supplementAudioOffset)/1000;
+    }
     setScrubValue(parseFloat(event.target.value));
   };
 
   const handleVolumeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const volume = Number(event.target.value) / 100;
     const video = videoRef.current;
-    if (!video) return;
-    video.volume = Number(event.target.value) / 100;
+    const audio = supplementAudioRef.current;
+    if (!video || !audio) return;
+
+    if (balance < 0) {
+      video.volume = volume * (1 + balance);
+      audio.volume = volume;
+    } else {
+      video.volume = volume;
+      audio.volume = volume * (1 - balance);
+    }
+  };
+
+  const handleVideoEnded = () => {
+    setIsPlaying(false);
+    supplementAudioRef.current?.pause();
   };
 
+  const syncSupplementAudioWithVideo = () => {
+    const video = videoRef.current;
+    const audio = supplementAudioRef.current;
+    if (!video || !audio) return;
+    if (supplementAudioOffset === "" || supplementAudioOffset == null) return;
+    audio.currentTime = video.currentTime + parseInt(supplementAudioOffset)/1000;
+  }
 
   // Handling drag and drop files
   const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
@@ -227,7 +315,7 @@ function KaraokePage() {
     event.preventDefault();
     setDragOver(false);
     const file = event.dataTransfer.files?.[0];
-    if(file.name.endsWith(".lrc")) {
+    if (file.name.endsWith(".lrc")) {
       const reader = new FileReader();
       reader.onload = (e) => {
         setLrcContent(e.target?.result as string);
@@ -235,44 +323,44 @@ function KaraokePage() {
       };
       reader.readAsText(file);
       toast.success("LRC file loaded successfully", { autoClose: 2000 });
-    }
-    else if(file.name.endsWith(".srv3")) {
+    } else if (file.name.endsWith(".srv3")) {
       const reader = new FileReader();
       reader.onload = (e) => {
         setCaptionsText(e.target?.result as string);
       };
       reader.readAsText(file);
       toast.success("SRV file loaded successfully", { autoClose: 2000 });
-    }
-    else if(file.type.startsWith("video") || file.type.startsWith("audio") ) {
+    } else if (file.type.startsWith("video") || file.type.startsWith("audio")) {
       const url = URL.createObjectURL(file);
       setVideoUrl(url);
       setCurrentMillisecond(0);
       setScrubValue(0);
       setIsPlaying(false);
-      toast.success("Video/Audio file loaded successfully", { autoClose: 2000 });
-    }
-    else{
+      toast.success("Video/Audio file loaded successfully", {
+        autoClose: 2000,
+      });
+    } else {
       toast.error("Unsupported file type", { autoClose: 2000 });
     }
-  }
-
-
+  };
 
   return (
     <Root>
       <ToastContainer />
-      {/*LRC viewer*/ }
+      {/*LRC viewer*/}
       <div style={{ display: "flex", width: "100%", height: "100vh" }}>
         <KaraokePlayer
           lrc={lrcContent}
           currentMillisecond={currentMillisecond}
         />
 
-
         {/* Ternary operation for if videoUrl has been set */}
         <div
-          style={{ flex: 1, position: "relative", backgroundColor: dragOver ? 'lightblue' : 'white', }}
+          style={{
+            flex: 1,
+            position: "relative",
+            backgroundColor: dragOver ? "lightblue" : "white",
+          }}
           onMouseEnter={() => setShowFileInputs(true)}
           onMouseLeave={() => setShowFileInputs(false)}
           onDragOver={handleDragOver}
@@ -286,17 +374,23 @@ function KaraokePage() {
                 ref={videoRef}
                 src={videoUrl}
                 style={{ position: "absolute", width: "100%", height: "100%" }}
+                onEnded={handleVideoEnded}
               />
-              <div 
-              style={{ width: '90%', height: '90%', margin: 'auto' }}
-              onClick={() => handlePlayPause()}
+              <audio
+                ref={supplementAudioRef}
+                src={supplementAudioUrl}
+                style={{ display: "none" }}
+              />
+              <div
+                style={{ width: "90%", height: "90%", margin: "auto" }}
+                onClick={() => handlePlayPause()}
               >
                 <CaptionsRenderer
                   srv3={captionsText}
                   currentTime={currentMillisecond / 1000}
                 />
               </div>
-              { /*Video control bar*/ }
+              {/*Video control bar*/}
               <div
                 style={{
                   position: "absolute",
@@ -382,13 +476,19 @@ function KaraokePage() {
                 display: "flex",
                 justifyContent: "center",
                 alignItems: "center",
-                flexDirection: "column"
+                flexDirection: "column",
               }}
             >
-              <h1 style={{ fontFamily: "Arial", fontWeight: "bold", fontSize: "32px" }}>
+              <h1
+                style={{
+                  fontFamily: "Arial",
+                  fontWeight: "bold",
+                  fontSize: "32px",
+                }}
+              >
                 {statusText}
               </h1>
-            {/* Show a placeholder while no video selected */}
+              {/* Show a placeholder while no video selected */}
 
               <p
                 style={{
@@ -397,10 +497,9 @@ function KaraokePage() {
                   fontFamily: "Arial",
                 }}
               >
-
                 Please select the video and lrc (lyrics) file <br />
                 (Drag and Drop them here, or use the menus below!)
-                <br/>
+                <br />
                 <StyledLink href="/about>">About</StyledLink>
               </p>
             </div>
@@ -432,13 +531,28 @@ function KaraokePage() {
                 accept="video/*"
                 onChange={handleVideoFileChange}
               />
-              <FileInputLabel 
-                htmlFor="srvUpload"
+              <FileInputLabel htmlFor="srvUpload" style={{ cursor: "pointer" }}>
+                SRV
+              </FileInputLabel>
+              <FileInput
+                id="srvUpload"
+                type="file"
+                accept=".srv3"
+                onChange={handleSrvFileChange}
+              />
+              <FileInputLabel
+                htmlFor="supplementAudioUpload"
                 style={{ cursor: "pointer" }}
-                >
-                  SRV
+              >
+                Audio #2
               </FileInputLabel>
-              <FileInput id="srvUpload" type="file" accept=".srv3" onChange={handleSrvFileChange}/>
+              <FileInput
+                id="supplementAudioUpload"
+                type="file"
+                accept="audio/*"
+                onChange={handleSupplementAudioFileChange}
+              />
+              <ControlBarButton onClick={syncSupplementAudioWithVideo}>Sync Audio</ControlBarButton>
               <div
                 style={{
                   display: "flex",
@@ -446,6 +560,15 @@ function KaraokePage() {
                   fontFamily: "Arial",
                 }}
               >
+                <label>Audio/Video Balance</label>
+                <input
+                  type="range"
+                  min="-1"
+                  max="1"
+                  step="0.01"
+                  value={balance}
+                  onChange={(e) => setBalance(Number(e.target.value))}
+                />
                 <label>Offset (±ms) </label>
                 <input
                   type="number"
@@ -456,6 +579,23 @@ function KaraokePage() {
                   step="100"
                 />
               </div>
+              <div
+                style={{
+                  display: "flex",
+                  flexDirection: "column",
+                  fontFamily: "Arial",
+                }}
+              >
+                <label>Audio 2 Offset (±ms) </label>
+                <input
+                  type="number"
+                  style={{ fontSize: "14px" }}
+                  id="numberInput"
+                  value={supplementAudioOffset}
+                  onChange={(e) => setSupplementAudioOffset(e.target.value)}
+                  step="25"
+                />
+              </div>
             </FileInputContainer>
           )}
         </div>
-- 
cgit v1.2.3

