Download index.html from Hectozar/Record: direct link, hf CLI and curl.
- Browser
- Download file 67.4 kB
-
https://huggingface.co/spaces/Hectozar/Record/resolve/main/index.html
- Command line
-
hf download hf://spaces/Hectozar/Record/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Hectozar/Record/resolve/main/index.html
67.4 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ProAudio Studio - Advanced Web Recorder</title> | |
| <!-- Custom embedded SVG Favicon --> | |
| <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMjU2M2ViIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTEyIDJhMyAzIDAgMCAwLTMgM3Y3YTMgMyAwIDAgMCA2IDBWNWEzIDMgMCAwIDAtMy0zeiI+PC9wYXRoPjxwb2x5bGluZSBwb2ludHM9IjE5IDEwIDE5IDEyIDE5IDE0Ij48L3BvbHlsaW5lPjxsaW5lIHgxPSI1IiB5MT0iMTIiIHgyPSIxOSIgeTI9IjEyIj48L2xpbmU+PHBhdGggZD0iTTUgMTB2NGExIDEgMCAwIDAgMSAxIDEgMSAwIDAgMCAxLTF2LTQiPjwvcGF0aD48L3N2Zz4="> | |
| <style> | |
| /* ========================================================================== | |
| CSS VARIABLES & THEME (Ultra-Professional Light Mode) | |
| ========================================================================== */ | |
| :root { | |
| /* Core Colors */ | |
| --brand-primary: #2563eb; | |
| --brand-primary-hover: #1d4ed8; | |
| --brand-primary-light: #eff6ff; | |
| --brand-danger: #ef4444; | |
| --brand-danger-hover: #dc2626; | |
| --brand-success: #10b981; | |
| --brand-warning: #f59e0b; | |
| /* Backgrounds */ | |
| --bg-base: #ffffff; | |
| --bg-surface: #f8fafc; | |
| --bg-surface-hover: #f1f5f9; | |
| --bg-surface-active: #e2e8f0; | |
| --bg-modal: rgba(15, 23, 42, 0.5); | |
| /* Typography */ | |
| --text-strong: #0f172a; | |
| --text-normal: #334155; | |
| --text-muted: #64748b; | |
| /* Borders & Shadows */ | |
| --border-light: #e2e8f0; | |
| --border-medium: #cbd5e1; | |
| --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | |
| --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| /* Sizing & Radius */ | |
| --radius-md: 8px; | |
| --radius-lg: 12px; | |
| --radius-full: 9999px; | |
| --sidebar-width: 320px; | |
| /* Graph Colors */ | |
| --graph-color-1: #3b82f6; | |
| --graph-color-2: #60a5fa; | |
| /* Fonts */ | |
| --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| } | |
| /* ========================================================================== | |
| RESET & GLOBAL STYLES | |
| ========================================================================== */ | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: var(--font-sans); | |
| background-color: var(--bg-base); | |
| color: var(--text-strong); | |
| display: flex; | |
| height: 100vh; | |
| width: 100vw; | |
| overflow: hidden; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| } | |
| input, button, textarea { | |
| font-family: inherit; | |
| } | |
| /* Scrollbar styling */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border-medium); | |
| border-radius: var(--radius-full); | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--text-muted); | |
| } | |
| /* ========================================================================== | |
| TYPOGRAPHY & UTILITIES | |
| ========================================================================== */ | |
| h1, h2, h3 { font-weight: 600; color: var(--text-strong); } | |
| h1 { font-size: 28px; letter-spacing: -0.02em; } | |
| h2 { font-size: 20px; } | |
| p { color: var(--text-normal); line-height: 1.5; } | |
| .text-muted { color: var(--text-muted); font-size: 13px; } | |
| .hidden { display: none ; } | |
| .flex { display: flex; } | |
| .items-center { align-items: center; } | |
| .justify-between { justify-content: space-between; } | |
| .gap-2 { gap: 8px; } | |
| .gap-4 { gap: 16px; } | |
| /* ========================================================================== | |
| BUTTONS | |
| ========================================================================== */ | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 10px 18px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| border-radius: var(--radius-md); | |
| border: 1px solid transparent; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| user-select: none; | |
| } | |
| .btn:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| transform: none ; | |
| } | |
| .btn:active:not(:disabled) { | |
| transform: scale(0.98); | |
| } | |
| .btn-primary { background-color: var(--brand-primary); color: white; box-shadow: var(--shadow-sm); } | |
| .btn-primary:hover:not(:disabled) { background-color: var(--brand-primary-hover); } | |
| .btn-danger { background-color: var(--brand-danger); color: white; box-shadow: var(--shadow-sm); } | |
| .btn-danger:hover:not(:disabled) { background-color: var(--brand-danger-hover); } | |
| .btn-outline { background-color: transparent; border-color: var(--border-medium); color: var(--text-strong); } | |
| .btn-outline:hover:not(:disabled) { background-color: var(--bg-surface-hover); border-color: var(--text-muted); } | |
| .btn-ghost { background-color: transparent; color: var(--text-normal); padding: 8px; border-radius: var(--radius-md); } | |
| .btn-ghost:hover:not(:disabled) { background-color: var(--bg-surface-active); color: var(--text-strong); } | |
| .btn-icon { padding: 8px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: transparent; border: none; cursor: pointer; color: var(--text-muted); transition: all 0.2s; } | |
| .btn-icon:hover { background-color: var(--bg-surface-active); color: var(--text-strong); } | |
| .btn-icon.active { color: var(--brand-warning); } | |
| /* SVG Icon Sizing */ | |
| svg.icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } | |
| svg.icon-sm { width: 14px; height: 14px; } | |
| svg.icon-lg { width: 24px; height: 24px; } | |
| /* ========================================================================== | |
| LAYOUT - SIDEBAR (Gemini Style) | |
| ========================================================================== */ | |
| .sidebar { | |
| width: var(--sidebar-width); | |
| background-color: var(--bg-surface); | |
| border-right: 1px solid var(--border-light); | |
| display: flex; | |
| flex-direction: column; | |
| z-index: 10; | |
| } | |
| .sidebar-header { | |
| padding: 24px; | |
| border-bottom: 1px solid var(--border-light); | |
| } | |
| .logo-area { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 24px; | |
| } | |
| .logo-icon { | |
| width: 32px; | |
| height: 32px; | |
| background: var(--brand-primary-light); | |
| border-radius: var(--radius-md); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--brand-primary); | |
| } | |
| .logo-text { font-size: 18px; font-weight: 700; color: var(--text-strong); } | |
| .new-record-btn { width: 100%; justify-content: flex-start; padding: 12px 16px; font-size: 15px; } | |
| .sidebar-search { | |
| padding: 16px 24px; | |
| position: relative; | |
| } | |
| .sidebar-search input { | |
| width: 100%; | |
| padding: 10px 10px 10px 36px; | |
| border-radius: var(--radius-md); | |
| border: 1px solid var(--border-medium); | |
| background-color: var(--bg-base); | |
| font-size: 14px; | |
| transition: all 0.2s; | |
| } | |
| .sidebar-search input:focus { outline: none; border-color: var(--brand-primary); box-shadow: 0 0 0 3px var(--brand-primary-light); } | |
| .sidebar-search svg { position: absolute; left: 36px; top: 50%; transform: translateY(-50%); color: var(--text-muted); } | |
| .history-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 0 12px 24px 12px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .history-item { | |
| padding: 12px 16px; | |
| border-radius: var(--radius-md); | |
| display: flex; | |
| align-items: flex-start; | |
| justify-content: space-between; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| border: 1px solid transparent; | |
| } | |
| .history-item:hover { background-color: var(--bg-surface-hover); } | |
| .history-item.active { background-color: var(--brand-primary-light); border-color: #bfdbfe; } | |
| .history-info { display: flex; flex-direction: column; gap: 4px; flex: 1; overflow: hidden; } | |
| .history-title { font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-strong); } | |
| .history-meta { font-size: 12px; color: var(--text-muted); display: flex; gap: 8px; align-items: center; } | |
| /* ========================================================================== | |
| LAYOUT - MAIN CONTENT | |
| ========================================================================== */ | |
| .main-content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| background-color: var(--bg-base); | |
| position: relative; | |
| overflow-y: auto; | |
| } | |
| .topbar { | |
| padding: 24px 40px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| border-bottom: 1px solid var(--border-light); | |
| background: rgba(255, 255, 255, 0.9); | |
| backdrop-filter: blur(10px); | |
| position: sticky; | |
| top: 0; | |
| z-index: 5; | |
| } | |
| .workspace { | |
| padding: 40px; | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| width: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 40px; | |
| } | |
| /* ========================================================================== | |
| RECORDING CONSOLE & VISUALIZER | |
| ========================================================================== */ | |
| .recording-console { | |
| background-color: var(--bg-surface); | |
| border: 1px solid var(--border-light); | |
| border-radius: var(--radius-lg); | |
| padding: 40px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| box-shadow: var(--shadow-md); | |
| transition: all 0.3s ease; | |
| } | |
| .recording-console.is-recording { | |
| border-color: #fca5a5; | |
| box-shadow: 0 0 0 4px #fee2e2; | |
| } | |
| .visualizer-container { | |
| width: 100%; | |
| height: 160px; | |
| background-color: var(--text-strong); | |
| border-radius: var(--radius-md); | |
| overflow: hidden; | |
| position: relative; | |
| margin-bottom: 30px; | |
| box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); | |
| } | |
| canvas#liveVisualizer { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| } | |
| .recording-status { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 24px; | |
| } | |
| .status-dot { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| background-color: var(--text-muted); | |
| transition: all 0.3s; | |
| } | |
| .is-recording .status-dot { | |
| background-color: var(--brand-danger); | |
| animation: pulse-red 1.5s infinite; | |
| } | |
| .is-paused .status-dot { | |
| background-color: var(--brand-warning); | |
| } | |
| @keyframes pulse-red { | |
| 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } | |
| 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } | |
| 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } | |
| } | |
| .timer-display { | |
| font-size: 56px; | |
| font-weight: 300; | |
| font-variant-numeric: tabular-nums; | |
| letter-spacing: -1px; | |
| color: var(--text-strong); | |
| } | |
| .controls-bar { | |
| display: flex; | |
| gap: 16px; | |
| margin-top: 20px; | |
| } | |
| /* ========================================================================== | |
| PLAYBACK & EDITING CONSOLE (The Professional Editor) | |
| ========================================================================== */ | |
| .editor-console { | |
| background-color: var(--bg-base); | |
| border: 1px solid var(--border-medium); | |
| border-radius: var(--radius-lg); | |
| padding: 30px; | |
| box-shadow: var(--shadow-lg); | |
| display: none; | |
| flex-direction: column; | |
| gap: 24px; | |
| } | |
| .editor-console.active { | |
| display: flex; | |
| animation: fadeIn 0.4s ease forwards; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .editor-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| } | |
| .title-group { display: flex; align-items: center; gap: 12px; } | |
| .editable-title { | |
| font-size: 24px; | |
| font-weight: 600; | |
| border: 1px dashed transparent; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| transition: all 0.2s; | |
| cursor: pointer; | |
| } | |
| .editable-title:hover { border-color: var(--border-medium); background: var(--bg-surface); } | |
| .editable-title:focus { outline: none; border-color: var(--brand-primary); background: var(--bg-base); cursor: text; } | |
| .editor-actions { display: flex; gap: 8px; } | |
| /* Complex Static Waveform */ | |
| .waveform-container { | |
| width: 100%; | |
| height: 120px; | |
| background-color: var(--bg-surface-hover); | |
| border-radius: var(--radius-md); | |
| position: relative; | |
| cursor: pointer; | |
| border: 1px solid var(--border-light); | |
| overflow: hidden; | |
| } | |
| canvas#staticWaveform { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| pointer-events: none; | |
| } | |
| .playhead { | |
| position: absolute; | |
| top: 0; | |
| bottom: 0; | |
| width: 2px; | |
| background-color: var(--brand-danger); | |
| left: 0%; | |
| pointer-events: none; | |
| z-index: 2; | |
| box-shadow: 0 0 4px rgba(239, 68, 68, 0.5); | |
| } | |
| /* Editing / Trimming Controls */ | |
| .trim-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| background: var(--bg-surface); | |
| padding: 16px; | |
| border-radius: var(--radius-md); | |
| border: 1px solid var(--border-light); | |
| } | |
| .trim-group { display: flex; align-items: center; gap: 10px; } | |
| .trim-group label { font-size: 13px; font-weight: 600; color: var(--text-normal); text-transform: uppercase;} | |
| .trim-group input[type="number"] { | |
| width: 80px; | |
| padding: 8px; | |
| border: 1px solid var(--border-medium); | |
| border-radius: 4px; | |
| font-size: 14px; | |
| font-family: monospace; | |
| } | |
| .playback-controls { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 24px; | |
| } | |
| .play-btn-large { | |
| width: 56px; | |
| height: 56px; | |
| border-radius: 50%; | |
| background-color: var(--brand-primary); | |
| color: white; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border: none; | |
| cursor: pointer; | |
| box-shadow: var(--shadow-md); | |
| transition: all 0.2s; | |
| } | |
| .play-btn-large:hover { background-color: var(--brand-primary-hover); transform: scale(1.05); } | |
| .play-btn-large svg { width: 24px; height: 24px; fill: currentColor; } | |
| .export-section { | |
| display: flex; | |
| gap: 16px; | |
| padding-top: 20px; | |
| border-top: 1px solid var(--border-light); | |
| } | |
| .link-generator { | |
| flex: 1; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: var(--bg-surface); | |
| border: 1px solid var(--border-medium); | |
| border-radius: var(--radius-md); | |
| padding: 4px 4px 4px 12px; | |
| } | |
| .link-generator input { | |
| flex: 1; | |
| border: none; | |
| background: transparent; | |
| font-size: 13px; | |
| color: var(--text-muted); | |
| outline: none; | |
| } | |
| /* ========================================================================== | |
| MODALS & LOADING SCREEN | |
| ========================================================================== */ | |
| #loadingScreen { | |
| position: fixed; | |
| inset: 0; | |
| background-color: var(--bg-base); | |
| z-index: 9999; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| transition: opacity 0.6s ease; | |
| } | |
| .loader-wave { | |
| display: flex; | |
| gap: 6px; | |
| margin-bottom: 24px; | |
| } | |
| .loader-bar { | |
| width: 8px; | |
| height: 40px; | |
| background-color: var(--brand-primary); | |
| border-radius: 4px; | |
| animation: wave 1s ease-in-out infinite; | |
| } | |
| .loader-bar:nth-child(2) { animation-delay: 0.1s; } | |
| .loader-bar:nth-child(3) { animation-delay: 0.2s; } | |
| .loader-bar:nth-child(4) { animation-delay: 0.3s; } | |
| .loader-bar:nth-child(5) { animation-delay: 0.4s; } | |
| @keyframes wave { | |
| 0%, 100% { transform: scaleY(0.5); } | |
| 50% { transform: scaleY(1); } | |
| } | |
| /* Modals */ | |
| .modal-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background-color: var(--bg-modal); | |
| backdrop-filter: blur(4px); | |
| z-index: 1000; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| } | |
| .modal-overlay.active { | |
| display: flex; | |
| opacity: 1; | |
| } | |
| .modal-content { | |
| background-color: var(--bg-base); | |
| width: 100%; | |
| max-width: 400px; | |
| border-radius: var(--radius-lg); | |
| padding: 24px; | |
| box-shadow: var(--shadow-lg); | |
| transform: scale(0.95); | |
| transition: transform 0.2s; | |
| } | |
| .modal-overlay.active .modal-content { | |
| transform: scale(1); | |
| } | |
| .modal-title { font-size: 18px; font-weight: 600; margin-bottom: 12px; } | |
| .modal-body { color: var(--text-normal); margin-bottom: 24px; font-size: 14px; } | |
| .modal-footer { display: flex; justify-content: flex-end; gap: 12px; } | |
| /* Toast */ | |
| #toast { | |
| position: fixed; | |
| bottom: 24px; | |
| right: 24px; | |
| background-color: var(--text-strong); | |
| color: white; | |
| padding: 12px 24px; | |
| border-radius: var(--radius-md); | |
| font-size: 14px; | |
| box-shadow: var(--shadow-md); | |
| transform: translateY(100px); | |
| opacity: 0; | |
| transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); | |
| z-index: 2000; | |
| } | |
| #toast.show { transform: translateY(0); opacity: 1; } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ========================================== | |
| LOADING SCREEN | |
| ========================================== --> | |
| <div id="loadingScreen"> | |
| <div class="loader-wave"> | |
| <div class="loader-bar"></div> | |
| <div class="loader-bar"></div> | |
| <div class="loader-bar"></div> | |
| <div class="loader-bar"></div> | |
| <div class="loader-bar"></div> | |
| </div> | |
| <h2>Initializing Audio Engine...</h2> | |
| <p class="text-muted" style="margin-top: 10px;">Setting up local workspace</p> | |
| </div> | |
| <!-- ========================================== | |
| SIDEBAR (WORKSPACE) | |
| ========================================== --> | |
| <aside class="sidebar"> | |
| <div class="sidebar-header"> | |
| <div class="logo-area"> | |
| <div class="logo-icon"> | |
| <svg class="icon" viewBox="0 0 24 24"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><polyline points="19 10 19 12 19 14"/><line x1="5" y1="12" x2="19" y2="12"/><path d="M5 10v4a1 1 0 0 0 1 1 1 1 0 0 0 1-1v-4"/></svg> | |
| </div> | |
| <div class="logo-text">ProAudio Studio</div> | |
| </div> | |
| <button class="btn btn-primary new-record-btn" id="navNewRecordBtn"> | |
| <svg class="icon" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> | |
| New Recording | |
| </button> | |
| </div> | |
| <div class="sidebar-search"> | |
| <svg class="icon icon-sm" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> | |
| <input type="text" id="searchInput" placeholder="Search recordings..."> | |
| </div> | |
| <div class="history-list" id="historyList"> | |
| <!-- Dynamic items injected here via JS --> | |
| </div> | |
| </aside> | |
| <!-- ========================================== | |
| MAIN CONTENT AREA | |
| ========================================== --> | |
| <main class="main-content"> | |
| <header class="topbar"> | |
| <h2>Workspace</h2> | |
| <div class="text-muted" id="storageInfo">Local Storage Active</div> | |
| </header> | |
| <div class="workspace"> | |
| <!-- RECORDING CONSOLE --> | |
| <section class="recording-console" id="recordingConsole"> | |
| <div class="visualizer-container"> | |
| <canvas id="liveVisualizer"></canvas> | |
| </div> | |
| <div class="recording-status"> | |
| <div class="status-dot"></div> | |
| <div class="text-muted" id="statusText">Ready to Record</div> | |
| </div> | |
| <div class="timer-display" id="liveTimer">00:00:00</div> | |
| <div class="controls-bar"> | |
| <button class="btn btn-danger" id="recordBtn"> | |
| <svg class="icon" viewBox="0 0 24 24"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg> | |
| Start Recording | |
| </button> | |
| <button class="btn btn-outline hidden" id="pauseBtn"> | |
| <svg class="icon" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> | |
| Pause | |
| </button> | |
| <button class="btn btn-primary hidden" id="stopBtn"> | |
| <svg class="icon" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/></svg> | |
| Stop & Save | |
| </button> | |
| </div> | |
| </section> | |
| <!-- EDITOR / PLAYBACK CONSOLE --> | |
| <section class="editor-console" id="editorConsole"> | |
| <div class="editor-header"> | |
| <div class="title-group"> | |
| <svg class="icon icon-lg" style="color:var(--brand-primary)" viewBox="0 0 24 24"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/></svg> | |
| <!-- Editable Title --> | |
| <h2 class="editable-title" id="editTitle" contenteditable="true" spellcheck="false" title="Click to rename">Recording Name</h2> | |
| <button class="btn-icon" id="renameTriggerBtn" title="Rename"><svg class="icon icon-sm" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button> | |
| </div> | |
| <div class="editor-actions"> | |
| <button class="btn-icon" id="editorFavBtn" title="Favorite"><svg class="icon" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg></button> | |
| <button class="btn-icon" id="editorDeleteBtn" title="Delete" style="color:var(--brand-danger)"><svg class="icon" viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg></button> | |
| </div> | |
| </div> | |
| <!-- Static Waveform --> | |
| <div class="waveform-container" id="waveformContainer"> | |
| <canvas id="staticWaveform"></canvas> | |
| <div class="playhead" id="playhead"></div> | |
| </div> | |
| <div style="display:flex; justify-content:space-between; align-items:center;"> | |
| <span class="text-muted" id="currentTimeDisplay">00:00.00</span> | |
| <span class="text-muted" id="totalDurationDisplay">00:00.00</span> | |
| </div> | |
| <!-- Advanced Trimming --> | |
| <div class="trim-controls"> | |
| <span style="font-size:13px; font-weight:600; color:var(--text-strong);">TRIM AUDIO</span> | |
| <div class="trim-group"> | |
| <label>Start (s)</label> | |
| <input type="number" id="trimStart" value="0.00" step="0.1" min="0"> | |
| </div> | |
| <div class="trim-group"> | |
| <label>End (s)</label> | |
| <input type="number" id="trimEnd" value="0.00" step="0.1" min="0"> | |
| </div> | |
| <button class="btn btn-outline" id="applyTrimBtn" style="padding:6px 12px; font-size:12px;">Apply Trim</button> | |
| </div> | |
| <div class="playback-controls"> | |
| <button class="btn-icon" id="rewindBtn"><svg class="icon" viewBox="0 0 24 24"><polygon points="11 19 2 12 11 5 11 19"/><polygon points="22 19 13 12 22 5 22 19"/></svg></button> | |
| <button class="play-btn-large" id="playPauseBtn"> | |
| <svg viewBox="0 0 24 24" id="playIconSVG"><polygon points="5 3 19 12 5 21 5 3"/></svg> | |
| </button> | |
| <button class="btn-icon" id="forwardBtn"><svg class="icon" viewBox="0 0 24 24"><polygon points="13 19 22 12 13 5 13 19"/><polygon points="2 19 11 12 2 5 2 19"/></svg></button> | |
| </div> | |
| <div class="export-section"> | |
| <button class="btn btn-primary" id="downloadBtn"> | |
| <svg class="icon" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> | |
| Download Audio | |
| </button> | |
| <div class="link-generator"> | |
| <svg class="icon icon-sm" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg> | |
| <input type="text" id="shareLinkInput" readonly value="blob:http://..."> | |
| <button class="btn btn-outline" id="copyLinkBtn" style="padding: 6px 12px; font-size: 12px;">Copy Link</button> | |
| </div> | |
| </div> | |
| <!-- Hidden Audio Element for playback --> | |
| <audio id="hiddenAudioPlayer" style="display:none;"></audio> | |
| </section> | |
| </div> | |
| </main> | |
| <!-- ========================================== | |
| MODALS & TOASTS | |
| ========================================== --> | |
| <!-- Delete Confirmation Modal --> | |
| <div class="modal-overlay" id="deleteModal"> | |
| <div class="modal-content"> | |
| <h3 class="modal-title">Delete Recording?</h3> | |
| <p class="modal-body">Are you sure you want to delete "<span id="deleteItemName" style="font-weight:600;"></span>"? This action cannot be undone and the file will be removed from your local database.</p> | |
| <div class="modal-footer"> | |
| <button class="btn btn-outline" id="cancelDeleteBtn">Cancel</button> | |
| <button class="btn btn-danger" id="confirmDeleteBtn">Delete</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Toast Notification --> | |
| <div id="toast">Notification Message</div> | |
| <!-- ========================================== | |
| JAVASCRIPT / APPLICATION LOGIC | |
| ========================================== --> | |
| <script> | |
| /** | |
| * PROAUDIO STUDIO APP | |
| * Massive monolithic architecture for advanced audio processing in browser. | |
| */ | |
| // --- 1. UTILITIES --- | |
| const Utils = { | |
| formatTime: (seconds) => { | |
| const h = Math.floor(seconds / 3600); | |
| const m = Math.floor((seconds % 3600) / 60); | |
| const s = Math.floor(seconds % 60); | |
| const ms = Math.floor((seconds % 1) * 100); | |
| const mStr = m.toString().padStart(2, '0'); | |
| const sStr = s.toString().padStart(2, '0'); | |
| if (h > 0) return `${h.toString().padStart(2, '0')}:${mStr}:${sStr}`; | |
| // If requesting ms precision (for editor) | |
| if (arguments[1] === true) return `${mStr}:${sStr}.${ms.toString().padStart(2, '0')}`; | |
| return `${mStr}:${sStr}`; | |
| }, | |
| formatDate: (timestamp) => { | |
| const d = new Date(timestamp); | |
| return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute:'2-digit' }); | |
| }, | |
| generateId: () => 'rec_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5), | |
| showToast: (message, duration = 3000) => { | |
| const toast = document.getElementById('toast'); | |
| toast.textContent = message; | |
| toast.classList.add('show'); | |
| setTimeout(() => toast.classList.remove('show'), duration); | |
| } | |
| }; | |
| // --- 2. DATABASE MANAGEMENT (IndexedDB) --- | |
| class Database { | |
| constructor() { | |
| this.dbName = 'ProAudioDB'; | |
| this.dbVersion = 1; | |
| this.storeName = 'recordings'; | |
| this.db = null; | |
| } | |
| init() { | |
| return new Promise((resolve, reject) => { | |
| const request = indexedDB.open(this.dbName, this.dbVersion); | |
| request.onupgradeneeded = (event) => { | |
| this.db = event.target.result; | |
| if (!this.db.objectStoreNames.contains(this.storeName)) { | |
| const store = this.db.createObjectStore(this.storeName, { keyPath: 'id' }); | |
| store.createIndex('timestamp', 'timestamp', { unique: false }); | |
| } | |
| }; | |
| request.onsuccess = (event) => { | |
| this.db = event.target.result; | |
| resolve(this.db); | |
| }; | |
| request.onerror = (event) => reject(event.target.error); | |
| }); | |
| } | |
| async saveRecord(record) { | |
| return new Promise((resolve, reject) => { | |
| const tx = this.db.transaction([this.storeName], 'readwrite'); | |
| const store = tx.objectStore(this.storeName); | |
| const request = store.put(record); | |
| request.onsuccess = () => resolve(record); | |
| request.onerror = () => reject(request.error); | |
| }); | |
| } | |
| async getAllRecords() { | |
| return new Promise((resolve, reject) => { | |
| const tx = this.db.transaction([this.storeName], 'readonly'); | |
| const store = tx.objectStore(this.storeName); | |
| const request = store.getAll(); | |
| request.onsuccess = () => { | |
| const records = request.result.sort((a, b) => b.timestamp - a.timestamp); | |
| resolve(records); | |
| }; | |
| request.onerror = () => reject(request.error); | |
| }); | |
| } | |
| async deleteRecord(id) { | |
| return new Promise((resolve, reject) => { | |
| const tx = this.db.transaction([this.storeName], 'readwrite'); | |
| const store = tx.objectStore(this.storeName); | |
| const request = store.delete(id); | |
| request.onsuccess = () => resolve(); | |
| request.onerror = () => reject(request.error); | |
| }); | |
| } | |
| async getRecord(id) { | |
| return new Promise((resolve, reject) => { | |
| const tx = this.db.transaction([this.storeName], 'readonly'); | |
| const store = tx.objectStore(this.storeName); | |
| const request = store.get(id); | |
| request.onsuccess = () => resolve(request.result); | |
| request.onerror = () => reject(request.error); | |
| }); | |
| } | |
| } | |
| // --- 3. ADVANCED AUDIO ENGINE (Web Audio API) --- | |
| class AudioEngine { | |
| constructor() { | |
| this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| async decodeAudioData(blob) { | |
| const arrayBuffer = await blob.arrayBuffer(); | |
| return await this.audioContext.decodeAudioData(arrayBuffer); | |
| } | |
| // High-performance waveform extraction | |
| getWaveformData(audioBuffer, points = 200) { | |
| const rawData = audioBuffer.getChannelData(0); // Left channel | |
| const samples = rawData.length; | |
| const blockSize = Math.floor(samples / points); | |
| const filteredData = []; | |
| for (let i = 0; i < points; i++) { | |
| let blockStart = blockSize * i; | |
| let sum = 0; | |
| for (let j = 0; j < blockSize; j++) { | |
| sum += Math.abs(rawData[blockStart + j]); | |
| } | |
| filteredData.push(sum / blockSize); | |
| } | |
| // Normalize | |
| const multiplier = Math.max(...filteredData) ** -1; | |
| return filteredData.map(n => n * multiplier); | |
| } | |
| // Trims an AudioBuffer and encodes back to WAV blob | |
| async trimAudioBuffer(buffer, startSec, endSec) { | |
| const rate = buffer.sampleRate; | |
| const startOffset = Math.max(0, Math.floor(startSec * rate)); | |
| const endOffset = Math.min(buffer.length, Math.floor(endSec * rate)); | |
| const frameCount = endOffset - startOffset; | |
| if (frameCount <= 0) throw new Error("Invalid trim range"); | |
| const newContext = new OfflineAudioContext(buffer.numberOfChannels, frameCount, rate); | |
| const newBuffer = newContext.createBuffer(buffer.numberOfChannels, frameCount, rate); | |
| for (let channel = 0; channel < buffer.numberOfChannels; channel++) { | |
| const channelData = buffer.getChannelData(channel); | |
| const newChannelData = newBuffer.getChannelData(channel); | |
| newChannelData.set(channelData.subarray(startOffset, endOffset)); | |
| } | |
| return this.bufferToWave(newBuffer, frameCount); | |
| } | |
| // Convert AudioBuffer to WAV format blob | |
| bufferToWave(abuffer, len) { | |
| const numOfChan = abuffer.numberOfChannels; | |
| const length = len * numOfChan * 2 + 44; | |
| const buffer = new ArrayBuffer(length); | |
| const view = new DataView(buffer); | |
| const channels = []; | |
| let i; | |
| let sample; | |
| let offset = 0; | |
| let pos = 0; | |
| // Write WAV header | |
| const setUint16 = (data) => { view.setUint16(pos, data, true); pos += 2; }; | |
| const setUint32 = (data) => { view.setUint32(pos, data, true); pos += 4; }; | |
| setUint32(0x46464952); // "RIFF" | |
| setUint32(length - 8); // file length - 8 | |
| setUint32(0x45564157); // "WAVE" | |
| setUint32(0x20746d66); // "fmt " chunk | |
| setUint32(16); // length = 16 | |
| setUint16(1); // PCM (uncompressed) | |
| setUint16(numOfChan); | |
| setUint32(abuffer.sampleRate); | |
| setUint32(abuffer.sampleRate * 2 * numOfChan); // avg. bytes/sec | |
| setUint16(numOfChan * 2); // block-align | |
| setUint16(16); // 16-bit | |
| setUint32(0x61746164); // "data" - chunk | |
| setUint32(length - pos - 4); // chunk length | |
| for(i = 0; i < abuffer.numberOfChannels; i++) channels.push(abuffer.getChannelData(i)); | |
| while(pos < length) { | |
| for(i = 0; i < numOfChan; i++) { | |
| sample = Math.max(-1, Math.min(1, channels[i][offset])); // clamp | |
| sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767)|0; // scale | |
| view.setInt16(pos, sample, true); | |
| pos += 2; | |
| } | |
| offset++; | |
| } | |
| return new Blob([buffer], {type: "audio/wav"}); | |
| } | |
| } | |
| // --- 4. VISUALIZER CLASS --- | |
| class Visualizer { | |
| constructor(canvasId) { | |
| this.canvas = document.getElementById(canvasId); | |
| this.ctx = this.canvas.getContext('2d'); | |
| this.animationId = null; | |
| this.resize(); | |
| window.addEventListener('resize', () => this.resize()); | |
| } | |
| resize() { | |
| const rect = this.canvas.parentElement.getBoundingClientRect(); | |
| this.canvas.width = rect.width * (window.devicePixelRatio || 1); | |
| this.canvas.height = rect.height * (window.devicePixelRatio || 1); | |
| this.ctx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1); | |
| this.width = rect.width; | |
| this.height = rect.height; | |
| this.clear(); | |
| } | |
| clear() { | |
| this.ctx.fillStyle = '#0f172a'; // dark background | |
| this.ctx.fillRect(0, 0, this.width, this.height); | |
| // Draw middle line | |
| this.ctx.beginPath(); | |
| this.ctx.moveTo(0, this.height/2); | |
| this.ctx.lineTo(this.width, this.height/2); | |
| this.ctx.strokeStyle = '#334155'; | |
| this.ctx.lineWidth = 1; | |
| this.ctx.stroke(); | |
| } | |
| drawLive(analyser) { | |
| const bufferLength = analyser.frequencyBinCount; | |
| const dataArray = new Uint8Array(bufferLength); | |
| const draw = () => { | |
| this.animationId = requestAnimationFrame(draw); | |
| analyser.getByteTimeDomainData(dataArray); | |
| this.ctx.fillStyle = '#0f172a'; | |
| this.ctx.fillRect(0, 0, this.width, this.height); | |
| this.ctx.lineWidth = 2; | |
| this.ctx.strokeStyle = '#3b82f6'; | |
| this.ctx.beginPath(); | |
| const sliceWidth = this.width * 1.0 / bufferLength; | |
| let x = 0; | |
| for(let i = 0; i < bufferLength; i++) { | |
| const v = dataArray[i] / 128.0; | |
| const y = v * this.height/2; | |
| if(i === 0) { | |
| this.ctx.moveTo(x, y); | |
| } else { | |
| this.ctx.lineTo(x, y); | |
| } | |
| x += sliceWidth; | |
| } | |
| this.ctx.lineTo(this.width, this.height/2); | |
| this.ctx.stroke(); | |
| // Add glow effect | |
| this.ctx.shadowBlur = 10; | |
| this.ctx.shadowColor = '#60a5fa'; | |
| this.ctx.stroke(); | |
| this.ctx.shadowBlur = 0; | |
| } | |
| draw(); | |
| } | |
| stop() { | |
| if(this.animationId) cancelAnimationFrame(this.animationId); | |
| this.clear(); | |
| } | |
| // Draw Static Waveform (For Editor) | |
| drawStatic(waveformData) { | |
| this.clear(); | |
| this.ctx.fillStyle = 'transparent'; // Use container background | |
| this.ctx.clearRect(0, 0, this.width, this.height); | |
| const barWidth = (this.width / waveformData.length); | |
| const centerY = this.height / 2; | |
| waveformData.forEach((val, i) => { | |
| const h = Math.max(2, val * this.height * 0.8); | |
| const x = i * barWidth; | |
| // Gradient based on height | |
| const grad = this.ctx.createLinearGradient(0, centerY - h/2, 0, centerY + h/2); | |
| grad.addColorStop(0, '#60a5fa'); | |
| grad.addColorStop(0.5, '#2563eb'); | |
| grad.addColorStop(1, '#60a5fa'); | |
| this.ctx.fillStyle = grad; | |
| // Rounded bars | |
| this.roundRect(this.ctx, x + 1, centerY - h/2, barWidth - 2, h, 2); | |
| this.ctx.fill(); | |
| }); | |
| } | |
| roundRect(ctx, x, y, width, height, radius) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x + radius, y); | |
| ctx.lineTo(x + width - radius, y); | |
| ctx.quadraticCurveTo(x + width, y, x + width, y + radius); | |
| ctx.lineTo(x + width, y + height - radius); | |
| ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); | |
| ctx.lineTo(x + radius, y + height); | |
| ctx.quadraticCurveTo(x, y + height, x, y + height - radius); | |
| ctx.lineTo(x, y + radius); | |
| ctx.quadraticCurveTo(x, y, x + radius, y); | |
| ctx.closePath(); | |
| } | |
| } | |
| // --- 5. MAIN APPLICATION CONTROLLER --- | |
| class App { | |
| constructor() { | |
| this.db = new Database(); | |
| this.audioEngine = new AudioEngine(); | |
| this.liveVisualizer = new Visualizer('liveVisualizer'); | |
| this.staticVisualizer = new Visualizer('staticWaveform'); | |
| // State variables | |
| this.mediaRecorder = null; | |
| this.audioChunks = []; | |
| this.startTime = null; | |
| this.timerInterval = null; | |
| this.elapsedSeconds = 0; | |
| this.currentStream = null; | |
| this.analyser = null; | |
| this.currentRecordId = null; | |
| this.currentAudioUrl = null; | |
| this.currentAudioBuffer = null; | |
| this.isPlaying = false; | |
| // Playback | |
| this.audioPlayer = document.getElementById('hiddenAudioPlayer'); | |
| this.init(); | |
| } | |
| async init() { | |
| try { | |
| await this.db.init(); | |
| this.bindEvents(); | |
| await this.renderHistory(); | |
| // Hide loading screen | |
| setTimeout(() => { | |
| document.getElementById('loadingScreen').style.opacity = '0'; | |
| setTimeout(() => document.getElementById('loadingScreen').style.display = 'none', 600); | |
| }, 1000); // 1 sec fake delay for feel | |
| } catch (err) { | |
| alert("Application failed to initialize. Please ensure browser storage is allowed."); | |
| console.error(err); | |
| } | |
| } | |
| bindEvents() { | |
| // Recording Controls | |
| document.getElementById('recordBtn').addEventListener('click', () => this.startRecording()); | |
| document.getElementById('pauseBtn').addEventListener('click', () => this.togglePauseRecording()); | |
| document.getElementById('stopBtn').addEventListener('click', () => this.stopRecording()); | |
| // Navigation / Sidebar | |
| document.getElementById('navNewRecordBtn').addEventListener('click', () => this.showRecordingConsole()); | |
| document.getElementById('searchInput').addEventListener('input', (e) => this.filterHistory(e.target.value)); | |
| // Editor Controls | |
| document.getElementById('playPauseBtn').addEventListener('click', () => this.togglePlayback()); | |
| document.getElementById('hiddenAudioPlayer').addEventListener('timeupdate', () => this.updatePlayhead()); | |
| document.getElementById('hiddenAudioPlayer').addEventListener('ended', () => { | |
| this.isPlaying = false; | |
| this.updatePlayIcon(); | |
| document.getElementById('playhead').style.left = '0%'; | |
| }); | |
| // Clicking on waveform to seek | |
| document.getElementById('waveformContainer').addEventListener('click', (e) => { | |
| if(!this.currentAudioBuffer) return; | |
| const rect = e.currentTarget.getBoundingClientRect(); | |
| const percent = (e.clientX - rect.left) / rect.width; | |
| this.audioPlayer.currentTime = percent * this.audioPlayer.duration; | |
| this.updatePlayhead(); | |
| }); | |
| // Rename Logic | |
| const titleEl = document.getElementById('editTitle'); | |
| titleEl.addEventListener('blur', () => this.saveRename()); | |
| titleEl.addEventListener('keydown', (e) => { if(e.key === 'Enter') { e.preventDefault(); titleEl.blur(); } }); | |
| document.getElementById('renameTriggerBtn').addEventListener('click', () => titleEl.focus()); | |
| // Editor Action Buttons | |
| document.getElementById('editorFavBtn').addEventListener('click', () => this.toggleFavoriteCurrent()); | |
| document.getElementById('editorDeleteBtn').addEventListener('click', () => this.promptDelete(this.currentRecordId)); | |
| document.getElementById('downloadBtn').addEventListener('click', () => this.downloadCurrent()); | |
| document.getElementById('copyLinkBtn').addEventListener('click', () => this.copyShareLink()); | |
| document.getElementById('applyTrimBtn').addEventListener('click', () => this.applyTrim()); | |
| // Modal | |
| document.getElementById('cancelDeleteBtn').addEventListener('click', () => this.closeModal()); | |
| document.getElementById('confirmDeleteBtn').addEventListener('click', () => this.executeDelete()); | |
| } | |
| // --- RECORDING LOGIC --- | |
| async startRecording() { | |
| try { | |
| this.currentStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } }); | |
| // Setup Web Audio nodes for visualizer | |
| const source = this.audioEngine.audioContext.createMediaStreamSource(this.currentStream); | |
| this.analyser = this.audioEngine.audioContext.createAnalyser(); | |
| this.analyser.fftSize = 2048; | |
| source.connect(this.analyser); | |
| this.liveVisualizer.drawLive(this.analyser); | |
| this.mediaRecorder = new MediaRecorder(this.currentStream); | |
| this.audioChunks = []; | |
| this.mediaRecorder.ondataavailable = (e) => { | |
| if (e.data.size > 0) this.audioChunks.push(e.data); | |
| }; | |
| this.mediaRecorder.onstop = () => this.processRecordingSave(); | |
| this.mediaRecorder.start(100); // 100ms chunks | |
| this.elapsedSeconds = 0; | |
| this.updateTimerDisplay(); | |
| this.timerInterval = setInterval(() => { | |
| this.elapsedSeconds++; | |
| this.updateTimerDisplay(); | |
| }, 1000); | |
| this.updateUIRecording(true); | |
| } catch (err) { | |
| alert("Microphone access denied or unavailable."); | |
| console.error(err); | |
| } | |
| } | |
| togglePauseRecording() { | |
| if (this.mediaRecorder.state === "recording") { | |
| this.mediaRecorder.pause(); | |
| clearInterval(this.timerInterval); | |
| this.liveVisualizer.stop(); | |
| const btn = document.getElementById('pauseBtn'); | |
| btn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Resume'; | |
| document.getElementById('recordingConsole').className = 'recording-console is-paused'; | |
| document.getElementById('statusText').textContent = "Paused"; | |
| } else if (this.mediaRecorder.state === "paused") { | |
| this.mediaRecorder.resume(); | |
| this.timerInterval = setInterval(() => { | |
| this.elapsedSeconds++; | |
| this.updateTimerDisplay(); | |
| }, 1000); | |
| this.liveVisualizer.drawLive(this.analyser); | |
| const btn = document.getElementById('pauseBtn'); | |
| btn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Pause'; | |
| document.getElementById('recordingConsole').className = 'recording-console is-recording'; | |
| document.getElementById('statusText').textContent = "Recording..."; | |
| } | |
| } | |
| stopRecording() { | |
| if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") { | |
| this.mediaRecorder.stop(); | |
| clearInterval(this.timerInterval); | |
| this.currentStream.getTracks().forEach(track => track.stop()); | |
| this.liveVisualizer.stop(); | |
| this.updateUIRecording(false); | |
| } | |
| } | |
| async processRecordingSave() { | |
| const blob = new Blob(this.audioChunks, { type: 'audio/webm' }); | |
| const id = Utils.generateId(); | |
| const record = { | |
| id: id, | |
| name: `Recording ${Utils.formatDate(Date.now())}`, | |
| timestamp: Date.now(), | |
| duration: this.elapsedSeconds, | |
| blob: blob, | |
| favorite: false | |
| }; | |
| await this.db.saveRecord(record); | |
| Utils.showToast("Recording saved successfully"); | |
| await this.renderHistory(); | |
| this.loadRecordingIntoEditor(record); | |
| } | |
| updateTimerDisplay() { | |
| document.getElementById('liveTimer').textContent = Utils.formatTime(this.elapsedSeconds); | |
| } | |
| updateUIRecording(isRecording) { | |
| const recBtn = document.getElementById('recordBtn'); | |
| const pauseBtn = document.getElementById('pauseBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const consoleEl = document.getElementById('recordingConsole'); | |
| const statusTxt = document.getElementById('statusText'); | |
| if (isRecording) { | |
| recBtn.classList.add('hidden'); | |
| pauseBtn.classList.remove('hidden'); | |
| stopBtn.classList.remove('hidden'); | |
| consoleEl.className = 'recording-console is-recording'; | |
| statusTxt.textContent = "Recording..."; | |
| } else { | |
| recBtn.classList.remove('hidden'); | |
| pauseBtn.classList.add('hidden'); | |
| stopBtn.classList.add('hidden'); | |
| consoleEl.className = 'recording-console'; | |
| statusTxt.textContent = "Ready to Record"; | |
| document.getElementById('liveTimer').textContent = "00:00:00"; | |
| pauseBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Pause'; | |
| } | |
| } | |
| showRecordingConsole() { | |
| // Stop any playback | |
| this.audioPlayer.pause(); | |
| this.isPlaying = false; | |
| document.getElementById('editorConsole').classList.remove('active'); | |
| document.getElementById('recordingConsole').style.display = 'flex'; | |
| // Remove active class from history | |
| document.querySelectorAll('.history-item').forEach(el => el.classList.remove('active')); | |
| } | |
| // --- EDITOR & PLAYBACK LOGIC --- | |
| async loadRecordingIntoEditor(record) { | |
| // UI Switch | |
| document.getElementById('recordingConsole').style.display = 'none'; | |
| document.getElementById('editorConsole').classList.add('active'); | |
| // Highlight active item | |
| document.querySelectorAll('.history-item').forEach(el => el.classList.remove('active')); | |
| const hItem = document.getElementById(`hist_${record.id}`); | |
| if(hItem) hItem.classList.add('active'); | |
| // Set Data | |
| this.currentRecordId = record.id; | |
| document.getElementById('editTitle').textContent = record.name; | |
| // Update Favorite Icon | |
| const favBtn = document.getElementById('editorFavBtn'); | |
| if(record.favorite) { | |
| favBtn.classList.add('active'); | |
| favBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24" fill="currentColor"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>'; | |
| } else { | |
| favBtn.classList.remove('active'); | |
| favBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>'; | |
| } | |
| // Setup Audio | |
| if (this.currentAudioUrl) URL.revokeObjectURL(this.currentAudioUrl); | |
| this.currentAudioUrl = URL.createObjectURL(record.blob); | |
| this.audioPlayer.src = this.currentAudioUrl; | |
| document.getElementById('shareLinkInput').value = this.currentAudioUrl; | |
| // Process Waveform | |
| try { | |
| this.currentAudioBuffer = await this.audioEngine.decodeAudioData(record.blob); | |
| const waveData = this.audioEngine.getWaveformData(this.currentAudioBuffer, 150); | |
| this.staticVisualizer.drawStatic(waveData); | |
| const dur = this.currentAudioBuffer.duration; | |
| document.getElementById('totalDurationDisplay').textContent = Utils.formatTime(dur, true); | |
| document.getElementById('currentTimeDisplay').textContent = "00:00.00"; | |
| // Setup trim inputs | |
| document.getElementById('trimStart').value = 0; | |
| document.getElementById('trimEnd').value = dur.toFixed(2); | |
| document.getElementById('trimEnd').max = dur.toFixed(2); | |
| } catch(e) { | |
| console.error("Error decoding audio for visualizer", e); | |
| } | |
| // Reset Playback UI | |
| this.isPlaying = false; | |
| this.updatePlayIcon(); | |
| document.getElementById('playhead').style.left = '0%'; | |
| } | |
| togglePlayback() { | |
| if(!this.currentRecordId) return; | |
| if (this.isPlaying) { | |
| this.audioPlayer.pause(); | |
| } else { | |
| this.audioPlayer.play(); | |
| } | |
| this.isPlaying = !this.isPlaying; | |
| this.updatePlayIcon(); | |
| } | |
| updatePlayIcon() { | |
| const svg = document.getElementById('playIconSVG'); | |
| if (this.isPlaying) { | |
| // Pause Icon | |
| svg.innerHTML = '<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>'; | |
| } else { | |
| // Play Icon | |
| svg.innerHTML = '<polygon points="5 3 19 12 5 21 5 3"/>'; | |
| } | |
| } | |
| updatePlayhead() { | |
| const percent = (this.audioPlayer.currentTime / this.audioPlayer.duration) * 100; | |
| document.getElementById('playhead').style.left = `${percent}%`; | |
| document.getElementById('currentTimeDisplay').textContent = Utils.formatTime(this.audioPlayer.currentTime, true); | |
| } | |
| // --- EDITING & ACTIONS --- | |
| async applyTrim() { | |
| if(!this.currentAudioBuffer) return; | |
| const start = parseFloat(document.getElementById('trimStart').value); | |
| const end = parseFloat(document.getElementById('trimEnd').value); | |
| if (start >= end || start < 0 || end > this.currentAudioBuffer.duration) { | |
| Utils.showToast("Invalid trim parameters."); | |
| return; | |
| } | |
| Utils.showToast("Processing audio trim...", 1000); | |
| try { | |
| const newWavBlob = await this.audioEngine.trimAudioBuffer(this.currentAudioBuffer, start, end); | |
| // Update Database | |
| const record = await this.db.getRecord(this.currentRecordId); | |
| record.blob = newWavBlob; | |
| record.duration = end - start; | |
| record.name = record.name + " (Trimmed)"; | |
| await this.db.saveRecord(record); | |
| Utils.showToast("Trim applied successfully!"); | |
| // Reload | |
| await this.renderHistory(); | |
| this.loadRecordingIntoEditor(record); | |
| } catch(e) { | |
| console.error(e); | |
| Utils.showToast("Failed to trim audio."); | |
| } | |
| } | |
| async saveRename() { | |
| if(!this.currentRecordId) return; | |
| const newName = document.getElementById('editTitle').textContent.trim(); | |
| if(newName === "") return; | |
| const record = await this.db.getRecord(this.currentRecordId); | |
| if(record.name !== newName) { | |
| record.name = newName; | |
| await this.db.saveRecord(record); | |
| Utils.showToast("Renamed successfully"); | |
| this.renderHistory(); | |
| } | |
| } | |
| async toggleFavoriteCurrent() { | |
| if(!this.currentRecordId) return; | |
| const record = await this.db.getRecord(this.currentRecordId); | |
| record.favorite = !record.favorite; | |
| await this.db.saveRecord(record); | |
| this.renderHistory(); | |
| // Update UI instantly | |
| const favBtn = document.getElementById('editorFavBtn'); | |
| if(record.favorite) { | |
| favBtn.classList.add('active'); | |
| favBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24" fill="currentColor"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>'; | |
| } else { | |
| favBtn.classList.remove('active'); | |
| favBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>'; | |
| } | |
| } | |
| downloadCurrent() { | |
| if(!this.currentAudioUrl || !this.currentRecordId) return; | |
| const title = document.getElementById('editTitle').textContent.trim(); | |
| const a = document.createElement('a'); | |
| a.href = this.currentAudioUrl; | |
| // Try to infer type based on whether we trimmed it (trim outputs wav) | |
| const ext = this.currentAudioUrl.includes('wav') ? '.wav' : '.webm'; | |
| a.download = `${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_export${ext}`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| Utils.showToast("Download started"); | |
| } | |
| copyShareLink() { | |
| const input = document.getElementById('shareLinkInput'); | |
| input.select(); | |
| document.execCommand('copy'); | |
| Utils.showToast("Local Blob Link Copied!"); | |
| } | |
| // --- HISTORY & MODALS --- | |
| async renderHistory(filterText = '') { | |
| const records = await this.db.getAllRecords(); | |
| const list = document.getElementById('historyList'); | |
| list.innerHTML = ''; | |
| records.forEach(rec => { | |
| if (filterText && !rec.name.toLowerCase().includes(filterText.toLowerCase())) return; | |
| const div = document.createElement('div'); | |
| div.className = `history-item ${this.currentRecordId === rec.id ? 'active' : ''}`; | |
| div.id = `hist_${rec.id}`; | |
| const starIcon = rec.favorite ? | |
| '<svg class="icon-sm" style="color:var(--brand-warning)" viewBox="0 0 24 24" fill="currentColor"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>' : ''; | |
| div.innerHTML = ` | |
| <div class="history-info"> | |
| <span class="history-title">${rec.name}</span> | |
| <span class="history-meta"> | |
| ${Utils.formatTime(rec.duration)} • ${Utils.formatDate(rec.timestamp)} | |
| ${starIcon} | |
| </span> | |
| </div> | |
| `; | |
| div.onclick = () => this.loadRecordingIntoEditor(rec); | |
| list.appendChild(div); | |
| }); | |
| } | |
| filterHistory(text) { | |
| this.renderHistory(text); | |
| } | |
| promptDelete(id) { | |
| if(!id) return; | |
| this.itemToDelete = id; | |
| const name = document.getElementById('editTitle').textContent; | |
| document.getElementById('deleteItemName').textContent = name; | |
| document.getElementById('deleteModal').classList.add('active'); | |
| } | |
| closeModal() { | |
| document.getElementById('deleteModal').classList.remove('active'); | |
| this.itemToDelete = null; | |
| } | |
| async executeDelete() { | |
| if (this.itemToDelete) { | |
| await this.db.deleteRecord(this.itemToDelete); | |
| Utils.showToast("Recording deleted"); | |
| this.closeModal(); | |
| this.showRecordingConsole(); // go back to main view | |
| this.renderHistory(); | |
| } | |
| } | |
| } | |
| // Initialize App on Load | |
| window.addEventListener('DOMContentLoaded', () => { | |
| window.proAudioApp = new App(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |