From 28a8270d21eb794a5db43e0ff8d1100a98e2894b Mon Sep 17 00:00:00 2001 From: Daksh Bajaniya Date: Fri, 27 Mar 2026 14:32:24 +0530 Subject: [PATCH 01/39] feat: restore and refine loop feature in BeatPlayer - Reintegrate loop logic into useAudioEngine rAF loop for zero-latency execution - Add EPSILON buffer and isSeeking guard for smooth looping - Conditionally render loop UI only for upload and external beat sources --- README.md | 101 ++++ client/src/components/BeatPlayer.jsx | 503 ++++++++++-------- client/src/components/Navbar.jsx | 28 +- client/src/components/SessionCard.jsx | 19 +- client/src/components/YouTubePlayer.jsx | 42 ++ client/src/context/AudioEngineContext.jsx | 200 +++++++ client/src/hooks/useAudioEngine.js | 81 +++ client/src/index.css | 51 +- client/src/pages/Dashboard.jsx | 75 ++- client/src/pages/Home.jsx | 35 +- client/src/pages/Login.jsx | 30 +- client/src/pages/NewSession.jsx | 43 +- client/src/pages/SessionEditor.jsx | 364 +++++++------ client/src/pages/Signup.jsx | 36 +- server/controllers/sessionController.js | 25 +- server/package.json | 1 + .../uploads/18ca41e539e7f9556a097bf5fc32b534 | Bin 0 -> 26064 bytes 17 files changed, 1156 insertions(+), 478 deletions(-) create mode 100644 README.md create mode 100644 client/src/components/YouTubePlayer.jsx create mode 100644 client/src/context/AudioEngineContext.jsx create mode 100644 client/src/hooks/useAudioEngine.js create mode 100644 server/uploads/18ca41e539e7f9556a097bf5fc32b534 diff --git a/README.md b/README.md new file mode 100644 index 0000000..99602c9 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +
+

🌌 Draft16

+

The Ultimate Web-Based Writing Studio for Vocalists & Songwriters

+
+ +## πŸ“– Overview +Draft16 is a full-stack, aesthetically driven web application designed specifically for recording artists, rappers, and songwriters. It combines a distraction-free glassmorphic text editor with integrated beat playback, syllable tracking, rhyme dictionaries, and synchronized vocal recordingβ€”all in one browser window. + +Gone are the days of juggling a notes app, YouTube for beats, and a voice memo app. Draft16 brings the entire ideation studio into a single, cohesive, premium environment. + +## ✨ Key Features + +### πŸ“ Advanced CodeMirror Writing Workspace +- **Distraction-Free UI:** A deep-space dark mode aesthetic with frosted-glass panels and modern typography. +- **Section Highlighting:** Automatically styles structural markers like `[Hook]`, `[Verse]`, and `[Bridge]` so you can see song structure at a glance. +- **Multiple Drafts:** Create, drag-and-drop sort, and manage multiple drafts within a single session. +- **Auto-save:** Never lose a bar. The editor continuously syncs with the database. + +### 🧠 Intelligent Lyric Tools +- **Syllable Counting:** Visually highlights and counts syllables per line to help you stay in the pocket. +- **Rhyme Finder:** Integrated Datamuse API instantly fetches rhymes for selected words without switching tabs. +- **Rhyme Scheme Visualization:** Automatically detects and color-codes rhyme schemes (AABB, ABAB) at the end of each line using a custom CodeMirror plugin. + +### 🎧 Beat Integration & Navigation +- **BeatPlayer:** Paste a YouTube URL or upload an audio file to write alongside your instrumental. +- **Sync Markers & Looping:** Drop timestamps while the beat plays to quickly jump between sections. Highlight a section of the beat to loop dynamically. +- **Metronome:** Keep your timing sharp with an integrated visual metronome mapped to your custom BPM. + +### πŸŽ™οΈ In-Browser Recording (Takes) +- **Zero-Latency Recording:** Record vocal takes directly in the browser over your beats. +- **Sync Modes:** Record just your vocals or sync your vocals against the playing instrumental. +- **Cloud Storage:** Takes are gracefully encoded as `WebM` and securely uploaded to Cloudinary for instant playback across devices. + +## πŸ› οΈ Tech Stack + +**Frontend** +- **Framework:** [React 19](https://react.dev/) + [Vite](https://vitejs.dev/) +- **Styling:** [Tailwind CSS v4](https://tailwindcss.com/) (Custom Space-Glassmorphism theme & Fonts: *Outfit* & *Inter*) +- **Editor Core:** [CodeMirror 6](https://codemirror.net/) +- **Audio:** Web Audio API & MediaRecorder API +- **Routing:** React Router DOM +- **State Management:** React Hooks +- **Drag & Drop:** `@dnd-kit` + +**Backend** +- **Runtime:** Node.js + Express.js +- **Database:** MongoDB + Mongoose +- **Authentication:** JSON Web Tokens (JWT) & bcrypt +- **File Storage:** Cloudinary (via Multer) + +## πŸš€ Getting Started + +### Prerequisites +- Node.js (v18+) +- MongoDB connection string +- Cloudinary account credentials + +### Installation +1. **Clone the repository:** + ```bash + git clone https://github.com/yourusername/draft16.git + cd draft16 + ``` + +2. **Backend Setup:** + ```bash + cd server + npm install + ``` + Create a `.env` file in the `server` directory: + ```env + PORT=5000 + MONGO_URI=your_mongodb_connection_string + JWT_SECRET=your_jwt_secret + CLOUDINARY_CLOUD_NAME=your_cloud_name + CLOUDINARY_API_KEY=your_api_key + CLOUDINARY_API_SECRET=your_api_secret + ``` + Start the backend server: + ```bash + npm run dev + ``` + +3. **Frontend Setup:** + ```bash + cd ../client + npm install + ``` + Start the frontend development server: + ```bash + npm run dev + ``` + +4. **Launch Draft16:** + Navigate to `http://localhost:5173/` in your browser. + +## 🀝 Contributing +Contributions, issues, and feature requests are welcome! + +## πŸ“œ License +This project is licensed under the MIT License. diff --git a/client/src/components/BeatPlayer.jsx b/client/src/components/BeatPlayer.jsx index 7114d60..e0e0281 100644 --- a/client/src/components/BeatPlayer.jsx +++ b/client/src/components/BeatPlayer.jsx @@ -1,128 +1,35 @@ -import React, { forwardRef, useImperativeHandle, useRef, useState, useEffect } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; +import { useAudioEngine } from '../hooks/useAudioEngine'; -const BeatPlayer = forwardRef(({ beatSource, beatUrl }, ref) => { - const iframeRef = useRef(null); - +// Utility format: 65 -> "1:05" +const formatTime = (seconds) => { + if (seconds === null || seconds === undefined || isNaN(seconds)) return "0:00"; + const m = Math.floor(seconds / 60); + const s = Math.floor(seconds % 60); + return `${m}:${s.toString().padStart(2, '0')}`; +}; + +const BeatPlayer = ({ beatUrl, beatSource = 'upload' }) => { + const { audioRef, play, pause, seek, isPlaying, duration } = useAudioEngine(beatUrl); + + // UI time β€” driven by rAF, frozen during drag to prevent jitter + const [uiTime, setUiTime] = useState(0); + + // --- NEW: Loop Controls State --- + const [loopEnabled, setLoopEnabled] = useState(false); const [loopStart, setLoopStart] = useState(null); const [loopEnd, setLoopEnd] = useState(null); - const [loopEnabled, setLoopEnabled] = useState(false); - const [loopStartInput, setLoopStartInput] = useState(''); const [loopEndInput, setLoopEndInput] = useState(''); const [loopError, setLoopError] = useState(''); - - // Track current time - const currentTimeRef = useRef(0); - - // We need to receive messages back from the iframe to get current time - useEffect(() => { - const handleMessage = (event) => { - // Basic check, might need to be more robust for production - try { - const data = JSON.parse(event.data); - if (data.event === 'infoDelivery' && data.info) { - if (data.info.currentTime !== undefined) { - currentTimeRef.current = data.info.currentTime; - } - } - } catch (e) { - // Ignore parsing errors for non-JSON messages - } - }; - window.addEventListener('message', handleMessage); - return () => window.removeEventListener('message', handleMessage); - }, []); + // Sync state to ref for zero-latency rAF execution + const loopStateRef = useRef({ enabled: false, start: null, end: null }); useEffect(() => { - // Request current time from YouTube iframe periodically - const timeInterval = setInterval(() => { - if (iframeRef.current && iframeRef.current.contentWindow) { - // Ask YouTube player for current time - iframeRef.current.contentWindow.postMessage(JSON.stringify({ - event: 'listening' - }), '*'); - } - - const currentTime = currentTimeRef.current; - - if (loopEnabled && loopStart !== null && loopEnd !== null) { - if (currentTime >= loopEnd) { - if (iframeRef.current && iframeRef.current.contentWindow) { - iframeRef.current.contentWindow.postMessage(JSON.stringify({ - event: 'command', - func: 'seekTo', - args: [loopStart, true] - }), '*'); - } - } - } - }, 250); - - return () => clearInterval(timeInterval); + loopStateRef.current = { enabled: loopEnabled, start: loopStart, end: loopEnd }; }, [loopEnabled, loopStart, loopEnd]); - useImperativeHandle(ref, () => ({ - seekTo: (seconds) => { - if (iframeRef.current && iframeRef.current.contentWindow) { - iframeRef.current.contentWindow.postMessage(JSON.stringify({ - event: 'command', - func: 'seekTo', - args: [seconds, true] - }), '*'); - iframeRef.current.contentWindow.postMessage(JSON.stringify({ - event: 'command', - func: 'playVideo', - args: [] - }), '*'); - } - }, - getCurrentTime: () => { - return currentTimeRef.current; - }, - setLoop: (startSeconds, endSeconds) => { - if (endSeconds <= startSeconds) { - return { error: 'End time must be greater than start time.' }; - } - setLoopStart(startSeconds); - setLoopEnd(endSeconds); - setLoopEnabled(true); - return { success: true }; - }, - clearLoop: () => { - setLoopStart(null); - setLoopEnd(null); - setLoopEnabled(false); - }, - toggleLoop: () => { - setLoopEnabled(prev => !prev); - } - })); - - if (beatSource !== 'youtube' || !beatUrl) { - return null; - } - - const extractVideoId = (url) => { - try { - const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; - const match = url.match(regExp); - return (match && match[2].length === 11) ? match[2] : null; - } catch (e) { - return null; - } - }; - - const videoId = extractVideoId(beatUrl); - - if (!videoId) return null; - - const formatTime = (seconds) => { - if (seconds === null || seconds === undefined) return '--:--'; - const m = Math.floor(seconds / 60); - const s = Math.floor(seconds % 60); - return `${m}:${s.toString().padStart(2, '0')}`; - }; - + // --- Helpers for parsing --- const formatTimeInput = (value) => { const digits = value.replace(/\D/g, '').slice(0, 4); if (digits.length <= 2) { @@ -149,18 +56,22 @@ const BeatPlayer = forwardRef(({ beatSource, beatUrl }, ref) => { setLoopError("Please enter both start and end times."); return; } - if (start < 0) { + if (start < 0 || isNaN(start)) { setLoopError("Invalid start time."); return; } - if (end <= start) { + if (end <= start || isNaN(end)) { setLoopError("End must be after start."); return; } + if (duration > 0 && end > duration) { + setLoopError("End time exceeds track duration."); + return; + } setLoopStart(start); setLoopEnd(end); - setLoopEnabled(true); + setLoopEnabled(true); // Auto-enable loop automatically }; const handleClearLoop = () => { @@ -172,107 +83,283 @@ const BeatPlayer = forwardRef(({ beatSource, beatUrl }, ref) => { setLoopError(''); }; - return ( -
-
-