Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Audio Recorder & Player</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #0a0a0f; | |
| --bg-secondary: #12121a; | |
| --bg-tertiary: #1a1a25; | |
| --fg: #e8e8ed; | |
| --fg-muted: #6b6b7a; | |
| --accent: #00d4aa; | |
| --accent-glow: rgba(0, 212, 170, 0.3); | |
| --accent-secondary: #ff6b6b; | |
| --border: #2a2a3a; | |
| --card: #15151f; | |
| --danger: #ff4757; | |
| --warning: #ffa502; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Space Grotesk', sans-serif; | |
| background: var(--bg); | |
| color: var(--fg); | |
| min-height: 100vh; | |
| margin: 0; | |
| overflow-x: hidden; | |
| } | |
| .mono { | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| /* Background atmosphere */ | |
| .bg-atmosphere { | |
| position: fixed; | |
| inset: 0; | |
| pointer-events: none; | |
| z-index: 0; | |
| overflow: hidden; | |
| } | |
| .bg-atmosphere::before { | |
| content: ''; | |
| position: absolute; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: | |
| radial-gradient(ellipse at 20% 20%, rgba(0, 212, 170, 0.08) 0%, transparent 50%), | |
| radial-gradient(ellipse at 80% 80%, rgba(255, 107, 107, 0.05) 0%, transparent 50%), | |
| radial-gradient(ellipse at 50% 50%, rgba(0, 212, 170, 0.03) 0%, transparent 70%); | |
| animation: atmosphereRotate 60s linear infinite; | |
| } | |
| @keyframes atmosphereRotate { | |
| from { transform: rotate(0deg); } | |
| to { transform: rotate(360deg); } | |
| } | |
| .noise-overlay { | |
| position: fixed; | |
| inset: 0; | |
| pointer-events: none; | |
| z-index: 1; | |
| opacity: 0.03; | |
| background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); | |
| } | |
| /* Grid pattern */ | |
| .grid-pattern { | |
| position: fixed; | |
| inset: 0; | |
| pointer-events: none; | |
| z-index: 1; | |
| background-image: | |
| linear-gradient(rgba(0, 212, 170, 0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0, 212, 170, 0.03) 1px, transparent 1px); | |
| background-size: 50px 50px; | |
| mask-image: radial-gradient(ellipse at center, black 0%, transparent 70%); | |
| } | |
| .main-container { | |
| position: relative; | |
| z-index: 10; | |
| min-height: 100vh; | |
| padding: 2rem; | |
| } | |
| /* Header */ | |
| .header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 3rem; | |
| padding-bottom: 1.5rem; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .logo-icon { | |
| width: 40px; | |
| height: 40px; | |
| background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| box-shadow: 0 4px 20px var(--accent-glow); | |
| } | |
| .logo-text { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| letter-spacing: -0.02em; | |
| } | |
| .built-with { | |
| font-size: 0.75rem; | |
| color: var(--fg-muted); | |
| text-decoration: none; | |
| transition: color 0.2s; | |
| } | |
| .built-with:hover { | |
| color: var(--accent); | |
| } | |
| /* Audio Container */ | |
| .audio-wrapper { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| } | |
| .block-label { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| margin-bottom: 1rem; | |
| color: var(--fg-muted); | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| } | |
| .block-label svg { | |
| width: 18px; | |
| height: 18px; | |
| color: var(--accent); | |
| } | |
| .audio-container { | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| padding: 1.5rem; | |
| position: relative; | |
| overflow: hidden; | |
| transition: border-color 0.3s, box-shadow 0.3s; | |
| } | |
| .audio-container:hover { | |
| border-color: rgba(0, 212, 170, 0.3); | |
| } | |
| .audio-container.dragging { | |
| border-color: var(--accent); | |
| box-shadow: 0 0 40px var(--accent-glow); | |
| } | |
| .audio-container.dragging .drop-zone { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| /* Drop Zone */ | |
| .drop-zone { | |
| position: absolute; | |
| inset: 0; | |
| background: rgba(0, 212, 170, 0.1); | |
| backdrop-filter: blur(10px); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 1rem; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: all 0.3s; | |
| z-index: 20; | |
| border-radius: 20px; | |
| } | |
| .drop-zone-icon { | |
| width: 60px; | |
| height: 60px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| animation: dropPulse 1.5s ease-in-out infinite; | |
| } | |
| @keyframes dropPulse { | |
| 0%, 100% { transform: scale(1); opacity: 1; } | |
| 50% { transform: scale(1.1); opacity: 0.8; } | |
| } | |
| .drop-zone-text { | |
| font-size: 1.125rem; | |
| font-weight: 500; | |
| color: var(--fg); | |
| } | |
| /* Source Selector */ | |
| .source-selector { | |
| display: flex; | |
| gap: 0.5rem; | |
| margin-bottom: 1.5rem; | |
| background: var(--bg-secondary); | |
| padding: 0.375rem; | |
| border-radius: 12px; | |
| width: fit-content; | |
| } | |
| .source-btn { | |
| padding: 0.625rem 1.25rem; | |
| border-radius: 10px; | |
| border: none; | |
| background: transparent; | |
| color: var(--fg-muted); | |
| font-family: inherit; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .source-btn:hover { | |
| color: var(--fg); | |
| } | |
| .source-btn.active { | |
| background: var(--accent); | |
| color: var(--bg); | |
| box-shadow: 0 4px 15px var(--accent-glow); | |
| } | |
| .source-btn svg { | |
| width: 16px; | |
| height: 16px; | |
| } | |
| /* Upload Area */ | |
| .upload-area { | |
| border: 2px dashed var(--border); | |
| border-radius: 16px; | |
| padding: 3rem 2rem; | |
| text-align: center; | |
| transition: all 0.3s; | |
| cursor: pointer; | |
| } | |
| .upload-area:hover { | |
| border-color: var(--accent); | |
| background: rgba(0, 212, 170, 0.03); | |
| } | |
| .upload-icon { | |
| width: 64px; | |
| height: 64px; | |
| margin: 0 auto 1.5rem; | |
| background: var(--bg-tertiary); | |
| border-radius: 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--accent); | |
| transition: transform 0.3s; | |
| } | |
| .upload-area:hover .upload-icon { | |
| transform: translateY(-4px); | |
| } | |
| .upload-title { | |
| font-size: 1.125rem; | |
| font-weight: 600; | |
| margin-bottom: 0.5rem; | |
| } | |
| .upload-subtitle { | |
| color: var(--fg-muted); | |
| font-size: 0.875rem; | |
| } | |
| .upload-formats { | |
| margin-top: 1rem; | |
| display: flex; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| } | |
| .format-tag { | |
| padding: 0.25rem 0.75rem; | |
| background: var(--bg-secondary); | |
| border-radius: 20px; | |
| font-size: 0.75rem; | |
| color: var(--fg-muted); | |
| } | |
| /* Recorder */ | |
| .recorder-container { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 2rem; | |
| } | |
| .waveform-display { | |
| width: 100%; | |
| height: 120px; | |
| background: var(--bg-secondary); | |
| border-radius: 12px; | |
| margin-bottom: 1.5rem; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .waveform-canvas { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .recording-indicator { | |
| position: absolute; | |
| top: 1rem; | |
| right: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.375rem 0.75rem; | |
| background: rgba(255, 71, 87, 0.2); | |
| border-radius: 20px; | |
| font-size: 0.75rem; | |
| color: var(--danger); | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .recording-indicator.active { | |
| opacity: 1; | |
| } | |
| .recording-dot { | |
| width: 8px; | |
| height: 8px; | |
| background: var(--danger); | |
| border-radius: 50%; | |
| animation: recordingPulse 1s ease-in-out infinite; | |
| } | |
| @keyframes recordingPulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.3; } | |
| } | |
| .recorder-controls { | |
| display: flex; | |
| gap: 1rem; | |
| align-items: center; | |
| } | |
| .record-btn { | |
| width: 72px; | |
| height: 72px; | |
| border-radius: 50%; | |
| border: 3px solid var(--accent); | |
| background: transparent; | |
| color: var(--accent); | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| } | |
| .record-btn::before { | |
| content: ''; | |
| position: absolute; | |
| inset: -8px; | |
| border-radius: 50%; | |
| border: 2px solid transparent; | |
| transition: border-color 0.3s; | |
| } | |
| .record-btn:hover::before { | |
| border-color: rgba(0, 212, 170, 0.3); | |
| } | |
| .record-btn.recording { | |
| background: var(--danger); | |
| border-color: var(--danger); | |
| animation: recordBtnPulse 1.5s ease-in-out infinite; | |
| } | |
| @keyframes recordBtnPulse { | |
| 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.4); } | |
| 50% { box-shadow: 0 0 0 15px rgba(255, 71, 87, 0); } | |
| } | |
| .record-btn svg { | |
| width: 28px; | |
| height: 28px; | |
| } | |
| .control-btn { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 12px; | |
| border: 1px solid var(--border); | |
| background: var(--bg-secondary); | |
| color: var(--fg-muted); | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .control-btn:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| .control-btn:disabled { | |
| opacity: 0.3; | |
| cursor: not-allowed; | |
| } | |
| .control-btn svg { | |
| width: 20px; | |
| height: 20px; | |
| } | |
| .timer { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 1.5rem; | |
| font-weight: 500; | |
| color: var(--fg); | |
| min-width: 100px; | |
| text-align: center; | |
| } | |
| /* Audio Player */ | |
| .player-container { | |
| display: none; | |
| } | |
| .player-container.active { | |
| display: block; | |
| } | |
| .player-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 1rem; | |
| } | |
| .player-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .player-thumbnail { | |
| width: 48px; | |
| height: 48px; | |
| background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .player-name { | |
| font-weight: 500; | |
| margin-bottom: 0.25rem; | |
| } | |
| .player-meta { | |
| font-size: 0.75rem; | |
| color: var(--fg-muted); | |
| } | |
| .player-actions { | |
| display: flex; | |
| gap: 0.5rem; | |
| } | |
| .action-btn { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 8px; | |
| border: 1px solid var(--border); | |
| background: transparent; | |
| color: var(--fg-muted); | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .action-btn:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| .action-btn.danger:hover { | |
| border-color: var(--danger); | |
| color: var(--danger); | |
| } | |
| .action-btn svg { | |
| width: 16px; | |
| height: 16px; | |
| } | |
| .player-waveform { | |
| height: 80px; | |
| background: var(--bg-secondary); | |
| border-radius: 12px; | |
| margin-bottom: 1rem; | |
| position: relative; | |
| overflow: hidden; | |
| cursor: pointer; | |
| } | |
| .waveform-bars { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 2px; | |
| height: 100%; | |
| padding: 0 1rem; | |
| } | |
| .waveform-bar { | |
| width: 3px; | |
| background: var(--accent); | |
| border-radius: 2px; | |
| opacity: 0.5; | |
| transition: opacity 0.2s; | |
| } | |
| .waveform-bar.active { | |
| opacity: 1; | |
| } | |
| .progress-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| height: 100%; | |
| background: linear-gradient(90deg, rgba(0, 212, 170, 0.2), transparent); | |
| pointer-events: none; | |
| transition: width 0.1s; | |
| } | |
| .player-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .play-btn { | |
| width: 56px; | |
| height: 56px; | |
| border-radius: 50%; | |
| border: none; | |
| background: var(--accent); | |
| color: var(--bg); | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| box-shadow: 0 4px 20px var(--accent-glow); | |
| } | |
| .play-btn:hover { | |
| transform: scale(1.05); | |
| } | |
| .play-btn svg { | |
| width: 24px; | |
| height: 24px; | |
| } | |
| .time-display { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.875rem; | |
| color: var(--fg-muted); | |
| } | |
| .progress-bar { | |
| flex: 1; | |
| height: 4px; | |
| background: var(--bg-tertiary); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| cursor: pointer; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: var(--accent); | |
| border-radius: 2px; | |
| transition: width 0.1s; | |
| } | |
| .volume-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .volume-slider { | |
| width: 80px; | |
| height: 4px; | |
| -webkit-appearance: none; | |
| appearance: none; | |
| background: var(--bg-tertiary); | |
| border-radius: 2px; | |
| cursor: pointer; | |
| } | |
| .volume-slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 12px; | |
| height: 12px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| } | |
| /* Streaming Bar */ | |
| .streaming-bar { | |
| height: 3px; | |
| background: var(--bg-tertiary); | |
| border-radius: 0 0 20px 20px; | |
| margin: 0 -1.5rem -1.5rem; | |
| overflow: hidden; | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| } | |
| .streaming-progress { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--accent), var(--accent-secondary)); | |
| width: 0%; | |
| animation: streamingAnim 2s ease-in-out infinite; | |
| } | |
| @keyframes streamingAnim { | |
| 0% { width: 0%; } | |
| 50% { width: 70%; } | |
| 100% { width: 0%; } | |
| } | |
| /* Status Messages */ | |
| .status-message { | |
| padding: 0.75rem 1rem; | |
| border-radius: 10px; | |
| font-size: 0.875rem; | |
| margin-top: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .status-message.error { | |
| background: rgba(255, 71, 87, 0.1); | |
| color: var(--danger); | |
| border: 1px solid rgba(255, 71, 87, 0.2); | |
| } | |
| .status-message.success { | |
| background: rgba(0, 212, 170, 0.1); | |
| color: var(--accent); | |
| border: 1px solid rgba(0, 212, 170, 0.2); | |
| } | |
| /* Animations */ | |
| .fade-in { | |
| animation: fadeIn 0.5s ease-out forwards; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .stagger-1 { animation-delay: 0.1s; } | |
| .stagger-2 { animation-delay: 0.2s; } | |
| .stagger-3 { animation-delay: 0.3s; } | |
| /* Reduced motion */ | |
| @media (prefers-reduced-motion: reduce) { | |
| *, *::before, *::after { | |
| animation-duration: 0.01ms ; | |
| animation-iteration-count: 1 ; | |
| transition-duration: 0.01ms ; | |
| } | |
| } | |
| /* Responsive */ | |
| @media (max-width: 640px) { | |
| .main-container { | |
| padding: 1rem; | |
| } | |
| .header { | |
| flex-direction: column; | |
| gap: 1rem; | |
| text-align: center; | |
| } | |
| .audio-container { | |
| padding: 1rem; | |
| } | |
| .upload-area { | |
| padding: 2rem 1rem; | |
| } | |
| .recorder-controls { | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| .player-controls { | |
| flex-wrap: wrap; | |
| } | |
| .volume-control { | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| .source-selector { | |
| width: 100%; | |
| } | |
| .source-btn { | |
| flex: 1; | |
| justify-content: center; | |
| } | |
| } | |
| /* Focus states */ | |
| button:focus-visible, | |
| input:focus-visible { | |
| outline: 2px solid var(--accent); | |
| outline-offset: 2px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg-atmosphere"></div> | |
| <div class="noise-overlay"></div> | |
| <div class="grid-pattern"></div> | |
| <div class="main-container"> | |
| <header class="header fade-in"> | |
| <div class="logo"> | |
| <div class="logo-icon"> | |
| <i data-lucide="audio-waveform" style="width: 24px; height: 24px; color: white;"></i> | |
| </div> | |
| <span class="logo-text">Audio Studio</span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" class="built-with" target="_blank" rel="noopener"> | |
| Built with anycoder | |
| </a> | |
| </header> | |
| <div class="audio-wrapper"> | |
| <div class="block-label fade-in stagger-1"> | |
| <i data-lucide="music"></i> | |
| <span>Audio Input</span> | |
| </div> | |
| <div class="audio-container fade-in stagger-2" id="audioContainer"> | |
| <!-- Drop Zone --> | |
| <div class="drop-zone" id="dropZone"> | |
| <div class="drop-zone-icon"> | |
| <i data-lucide="upload" style="width: 28px; height: 28px; color: var(--bg);"></i> | |
| </div> | |
| <span class="drop-zone-text">Drop audio file here</span> | |
| </div> | |
| <!-- Source Selector --> | |
| <div class="source-selector"> | |
| <button class="source-btn active" data-source="microphone" id="micSource"> | |
| <i data-lucide="mic"></i> | |
| Microphone | |
| </button> | |
| <button class="source-btn" data-source="upload" id="uploadSource"> | |
| <i data-lucide="upload"></i> | |
| Upload | |
| </button> | |
| </div> | |
| <!-- Microphone Source --> | |
| <div id="microphonePanel"> | |
| <div class="recorder-container"> | |
| <div class="waveform-display"> | |
| <canvas class="waveform-canvas" id="waveformCanvas"></canvas> | |
| <div class="recording-indicator" id="recordingIndicator"> | |
| <span class="recording-dot"></span> | |
| <span>REC</span> | |
| </div> | |
| </div> | |
| <div class="recorder-controls"> | |
| <button class="control-btn" id="pauseBtn" disabled aria-label="Pause recording"> | |
| <i data-lucide="pause"></i> | |
| </button> | |
| <button class="record-btn" id="recordBtn" aria-label="Start recording"> | |
| <i data-lucide="mic" id="recordIcon"></i> | |
| </button> | |
| <button class="control-btn" id="stopBtn" disabled aria-label="Stop recording"> | |
| <i data-lucide="square"></i> | |
| </button> | |
| </div> | |
| <div class="timer" id="timer">00:00</div> | |
| </div> | |
| </div> | |
| <!-- Upload Source --> | |
| <div id="uploadPanel" style="display: none;"> | |
| <div class="upload-area" id="uploadArea"> | |
| <div class="upload-icon"> | |
| <i data-lucide="file-audio" style="width: 32px; height: 32px;"></i> | |
| </div> | |
| <div class="upload-title">Drop audio file or click to upload</div> | |
| <div class="upload-subtitle">Maximum file size: 50MB</div> | |
| <div class="upload-formats"> | |
| <span class="format-tag">MP3</span> | |
| <span class="format-tag">WAV</span> | |
| <span class="format-tag">OGG</span> | |
| <span class="format-tag">FLAC</span> | |
| <span class="format-tag">AAC</span> | |
| <span class="format-tag">WEBM</span> | |
| </div> | |
| <input type="file" id="fileInput" accept="audio/*" style="display: none;"> | |
| </div> | |
| </div> | |
| <!-- Audio Player --> | |
| <div class="player-container" id="playerContainer"> | |
| <div class="player-header"> | |
| <div class="player-info"> | |
| <div class="player-thumbnail"> | |
| <i data-lucide="music" style="width: 24px; height: 24px; color: white;"></i> | |
| </div> | |
| <div> | |
| <div class="player-name" id="playerName">Recording</div> | |
| <div class="player-meta" id="playerMeta">WAV - 44.1kHz</div> | |
| </div> | |
| </div> | |
| <div class="player-actions"> | |
| <button class="action-btn" id="downloadBtn" aria-label="Download"> | |
| <i data-lucide="download"></i> | |
| </button> | |
| <button class="action-btn danger" id="clearBtn" aria-label="Clear"> | |
| <i data-lucide="trash-2"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="player-waveform" id="playerWaveform"> | |
| <div class="progress-overlay" id="progressOverlay"></div> | |
| <div class="waveform-bars" id="waveformBars"></div> | |
| </div> | |
| <div class="player-controls"> | |
| <button class="play-btn" id="playBtn" aria-label="Play"> | |
| <i data-lucide="play" id="playIcon"></i> | |
| </button> | |
| <span class="time-display" id="currentTime">0:00</span> | |
| <div class="progress-bar" id="progressBar"> | |
| <div class="progress-fill" id="progressFill"></div> | |
| </div> | |
| <span class="time-display" id="totalTime">0:00</span> | |
| <div class="volume-control"> | |
| <i data-lucide="volume-2" style="width: 18px; height: 18px; color: var(--fg-muted);"></i> | |
| <input type="range" class="volume-slider" id="volumeSlider" min="0" max="100" value="80"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Status Message --> | |
| <div class="status-message error" id="statusMessage" style="display: none;"></div> | |
| <!-- Streaming Bar --> | |
| <div class="streaming-bar" id="streamingBar" style="display: none;"> | |
| <div class="streaming-progress"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <audio id="audioElement" style="display: none;"></audio> | |
| <script> | |
| // Initialize Lucide icons | |
| lucide.createIcons(); | |
| // State | |
| let audioContext = null; | |
| let analyser = null; | |
| let mediaRecorder = null; | |
| let audioChunks = []; | |
| let isRecording = false; | |
| let isPaused = false; | |
| let startTime = 0; | |
| let elapsedTime = 0; | |
| let timerInterval = null; | |
| let animationId = null; | |
| let currentAudioBlob = null; | |
| let currentAudioUrl = null; | |
| // DOM Elements | |
| const audioContainer = document.getElementById('audioContainer'); | |
| const dropZone = document.getElementById('dropZone'); | |
| const micSource = document.getElementById('micSource'); | |
| const uploadSource = document.getElementById('uploadSource'); | |
| const microphonePanel = document.getElementById('microphonePanel'); | |
| const uploadPanel = document.getElementById('uploadPanel'); | |
| const uploadArea = document.getElementById('uploadArea'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const waveformCanvas = document.getElementById('waveformCanvas'); | |
| const recordBtn = document.getElementById('recordBtn'); | |
| const recordIcon = document.getElementById('recordIcon'); | |
| const pauseBtn = document.getElementById('pauseBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const timer = document.getElementById('timer'); | |
| const recordingIndicator = document.getElementById('recordingIndicator'); | |
| const playerContainer = document.getElementById('playerContainer'); | |
| const playerName = document.getElementById('playerName'); | |
| const playerMeta = document.getElementById('playerMeta'); | |
| const playBtn = document.getElementById('playBtn'); | |
| const playIcon = document.getElementById('playIcon'); | |
| const currentTimeEl = document.getElementById('currentTime'); | |
| const totalTimeEl = document.getElementById('totalTime'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const progressFill = document.getElementById('progressFill'); | |
| const progressOverlay = document.getElementById('progressOverlay'); | |
| const volumeSlider = document.getElementById('volumeSlider'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const clearBtn = document.getElementById('clearBtn'); | |
| const statusMessage = document.getElementById('statusMessage'); | |
| const streamingBar = document.getElementById('streamingBar'); | |
| const audioElement = document.getElementById('audioElement'); | |
| const waveformBars = document.getElementById('waveformBars'); | |
| // Canvas setup | |
| const ctx = waveformCanvas.getContext('2d'); | |
| function resizeCanvas() { | |
| const rect = waveformCanvas.getBoundingClientRect(); | |
| waveformCanvas.width = rect.width * window.devicePixelRatio; | |
| waveformCanvas.height = rect.height * window.devicePixelRatio; | |
| ctx.scale(window.devicePixelRatio, window.devicePixelRatio); | |
| } | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| // Source switching | |
| micSource.addEventListener('click', () => { | |
| micSource.classList.add('active'); | |
| uploadSource.classList.remove('active'); | |
| microphonePanel.style.display = 'block'; | |
| uploadPanel.style.display = 'none'; | |
| }); | |
| uploadSource.addEventListener('click', () => { | |
| uploadSource.classList.add('active'); | |
| micSource.classList.remove('active'); | |
| uploadPanel.style.display = 'block'; | |
| microphonePanel.style.display = 'none'; | |
| }); | |
| // Drag and drop | |
| audioContainer.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| audioContainer.classList.add('dragging'); | |
| }); | |
| audioContainer.addEventListener('dragleave', (e) => { | |
| e.preventDefault(); | |
| audioContainer.classList.remove('dragging'); | |
| }); | |
| audioContainer.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| audioContainer.classList.remove('dragging'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0 && files[0].type.startsWith('audio/')) { | |
| handleFile(files[0]); | |
| } | |
| }); | |
| // Upload area click | |
| uploadArea.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) { | |
| handleFile(e.target.files[0]); | |
| } | |
| }); | |
| // Handle file upload | |
| function handleFile(file) { | |
| if (file.size > 50 * 1024 * 1024) { | |
| showStatus('File size exceeds 50MB limit', 'error'); | |
| return; | |
| } | |
| currentAudioBlob = file; | |
| currentAudioUrl = URL.createObjectURL(file); | |
| playerName.textContent = file.name; | |
| playerMeta.textContent = `${file.type.split('/')[1].toUpperCase()} - ${(file.size / 1024 / 1024).toFixed(2)} MB`; | |
| audioElement.src = currentAudioUrl; | |
| audioElement.addEventListener('loadedmetadata', () => { | |
| totalTimeEl.textContent = formatTime(audioElement.duration); | |
| generateWaveform(); | |
| }); | |
| showPlayer(); | |
| showStatus('Audio file loaded successfully', 'success'); | |
| } | |
| // Recording functions | |
| async function startRecording() { | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| analyser = audioContext.createAnalyser(); | |
| analyser.fftSize = 256; | |
| const source = audioContext.createMediaStreamSource(stream); | |
| source.connect(analyser); | |
| mediaRecorder = new MediaRecorder(stream); | |
| audioChunks = []; | |
| mediaRecorder.ondataavailable = (e) => { | |
| audioChunks.push(e.data); | |
| }; | |
| mediaRecorder.onstop = () => { | |
| const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); | |
| currentAudioBlob = audioBlob; | |
| currentAudioUrl = URL.createObjectURL(audioBlob); | |
| playerName.textContent = 'Recording'; | |
| playerMeta.textContent = 'WAV - Recorded audio'; | |
| audioElement.src = currentAudioUrl; | |
| audioElement.addEventListener('loadedmetadata', () => { | |
| totalTimeEl.textContent = formatTime(audioElement.duration); | |
| generateWaveform(); | |
| }); | |
| showPlayer(); | |
| }; | |
| mediaRecorder.start(); | |
| isRecording = true; | |
| startTime = Date.now(); | |
| startTimer(); | |
| visualize(); | |
| recordBtn.classList.add('recording'); | |
| recordIcon.setAttribute('data-lucide', 'square'); | |
| lucide.createIcons(); | |
| pauseBtn.disabled = false; | |
| stopBtn.disabled = false; | |
| recordingIndicator.classList.add('active'); | |
| } catch (err) { | |
| console.error('Error accessing microphone:', err); | |
| showStatus('Could not access microphone. Please check permissions.', 'error'); | |
| } | |
| } | |
| function stopRecording() { | |
| if (mediaRecorder && mediaRecorder.state !== 'inactive') { | |
| mediaRecorder.stop(); | |
| mediaRecorder.stream.getTracks().forEach(track => track.stop()); | |
| } | |
| isRecording = false; | |
| isPaused = false; | |
| clearInterval(timerInterval); | |
| cancelAnimationFrame(animationId); | |
| recordBtn.classList.remove('recording'); | |
| recordIcon.setAttribute('data-lucide', 'mic'); | |
| lucide.createIcons(); | |
| pauseBtn.disabled = true; | |
| stopBtn.disabled = true; | |
| recordingIndicator.classList.remove('active'); | |
| if (audioContext) { | |
| audioContext.close(); | |
| } | |
| clearCanvas(); | |
| } | |
| function pauseRecording() { | |
| if (mediaRecorder && mediaRecorder.state === 'recording') { | |
| mediaRecorder.pause(); | |
| isPaused = true; | |
| clearInterval(timerInterval); | |
| pauseBtn.innerHTML = '<i data-lucide="play"></i>'; | |
| lucide.createIcons(); | |
| } else if (mediaRecorder && mediaRecorder.state === 'paused') { | |
| mediaRecorder.resume(); | |
| isPaused = false; | |
| startTimer(); | |
| pauseBtn.innerHTML = '<i data-lucide="pause"></i>'; | |
| lucide.createIcons(); | |
| } | |
| } | |
| // Timer | |
| function startTimer() { | |
| timerInterval = setInterval(() => { | |
| elapsedTime = Date.now() - startTime; | |
| timer.textContent = formatTime(elapsedTime / 1000); | |
| }, 100); | |
| } | |
| function formatTime(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = Math.floor(seconds % 60); | |
| return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| // Visualization | |
| function visualize() { | |
| if (!analyser) return; | |
| const bufferLength = analyser.frequencyBinCount; | |
| const dataArray = new Uint8Array(bufferLength); | |
| const width = waveformCanvas.width / window.devicePixelRatio; | |
| const height = waveformCanvas.height / window.devicePixelRatio; | |
| function draw() { | |
| if (!isRecording) return; | |
| animationId = requestAnimationFrame(draw); | |
| analyser.getByteFrequencyData(dataArray); | |
| ctx.fillStyle = '#12121a'; | |
| ctx.fillRect(0, 0, width, height); | |
| const barWidth = (width / bufferLength) * 2.5; | |
| let x = 0; | |
| for (let i = 0; i < bufferLength; i++) { | |
| const barHeight = (dataArray[i] / 255) * height * 0.8; | |
| const gradient = ctx.createLinearGradient(0, height - barHeight, 0, height); | |
| gradient.addColorStop(0, '#00d4aa'); | |
| gradient.addColorStop(1, 'rgba(0, 212, 170, 0.2)'); | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(x, height - barHeight, barWidth - 1, barHeight); | |
| x += barWidth; | |
| } | |
| } | |
| draw(); | |
| } | |
| function clearCanvas() { | |
| const width = waveformCanvas.width / window.devicePixelRatio; | |
| const height = waveformCanvas.height / window.devicePixelRatio; | |
| ctx.fillStyle = '#12121a'; | |
| ctx.fillRect(0, 0, width, height); | |
| } | |
| // Generate static waveform for player | |
| function generateWaveform() { | |
| waveformBars.innerHTML = ''; | |
| const barCount = 100; | |
| for (let i = 0; i < barCount; i++) { | |
| const bar = document.createElement('div'); | |
| bar.className = 'waveform-bar'; | |
| bar.style.height = `${Math.random() * 50 + 20}px`; | |
| waveformBars.appendChild(bar); | |
| } | |
| } | |
| // Player controls | |
| playBtn.addEventListener('click', () => { | |
| if (audioElement.paused) { | |
| audioElement.play(); | |
| playIcon.setAttribute('data-lucide', 'pause'); | |
| lucide.createIcons(); | |
| } else { | |
| audioElement.pause(); | |
| playIcon.setAttribute('data-lucide', 'play'); | |
| lucide.createIcons(); | |
| } | |
| }); | |
| audioElement.addEventListener('timeupdate', () => { | |
| const progress = (audioElement.currentTime / audioElement.duration) * 100; | |
| progressFill.style.width = `${progress}%`; | |
| progressOverlay.style.width = `${progress}%`; | |
| currentTimeEl.textContent = formatTime(audioElement.currentTime); | |
| // Update waveform bars | |
| const bars = waveformBars.querySelectorAll('.waveform-bar'); | |
| const activeIndex = Math.floor((audioElement.currentTime / audioElement.duration) * bars.length); | |
| bars.forEach((bar, i) => { | |
| bar.classList.toggle('active', i <= activeIndex); | |
| }); | |
| }); | |
| audioElement.addEventListener('ended', () => { | |
| playIcon.setAttribute('data-lucide', 'play'); | |
| lucide.createIcons(); | |
| }); | |
| progressBar.addEventListener('click', (e) => { | |
| const rect = progressBar.getBoundingClientRect(); | |
| const pos = (e.clientX - rect.left) / rect.width; | |
| audioElement.currentTime = pos * audioElement.duration; | |
| }); | |
| playerWaveform.addEventListener('click', (e) => { | |
| const rect = playerWaveform.getBoundingClientRect(); | |
| const pos = (e.clientX - rect.left) / rect.width; | |
| audioElement.currentTime = pos * audioElement.duration; | |
| }); | |
| volumeSlider.addEventListener('input', (e) => { | |
| audioElement.volume = e.target.value / 100; | |
| }); | |
| // Download | |
| downloadBtn.addEventListener('click', () => { | |
| if (currentAudioBlob) { | |
| const url = URL.createObjectURL(currentAudioBlob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'recording.wav'; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| }); | |
| // Clear | |
| clearBtn.addEventListener('click', () => { | |
| audioElement.pause(); | |
| audioElement.src = ''; | |
| currentAudioBlob = null; | |
| if (currentAudioUrl) { | |
| URL.revokeObjectURL(currentAudioUrl); | |
| currentAudioUrl = null; | |
| } | |
| hidePlayer(); | |
| timer.textContent = '00:00'; | |
| elapsedTime = 0; | |
| }); | |
| // Event listeners | |
| recordBtn.addEventListener('click', () => { | |
| if (isRecording) { | |
| stopRecording(); | |
| } else { | |
| startRecording(); | |
| } | |
| }); | |
| pauseBtn.addEventListener('click', pauseRecording); | |
| stopBtn.addEventListener('click', stopRecording); | |
| // Helper functions | |
| function showPlayer() { | |
| playerContainer.classList.add('active'); | |
| microphonePanel.style.display = 'none'; | |
| uploadPanel.style.display = 'none'; | |
| micSource.style.display = 'none'; | |
| uploadSource.style.display = 'none'; | |
| } | |
| function hidePlayer() { | |
| playerContainer.classList.remove('active'); | |
| const activeSource = micSource.classList.contains('active') ? 'mic' : 'upload'; | |
| if (activeSource === 'mic') { | |
| microphonePanel.style.display = 'block'; | |
| } else { | |
| uploadPanel.style.display = 'block'; | |
| } | |
| micSource.style.display = 'flex'; | |
| uploadSource.style.display = 'flex'; | |
| } | |
| function showStatus(message, type) { | |
| statusMessage.textContent = message; | |
| statusMessage.className = `status-message ${type}`; | |
| statusMessage.style.display = 'flex'; | |
| setTimeout(() => { | |
| statusMessage.style.display = 'none'; | |
| }, 3000); | |
| } | |
| // Initial canvas clear | |
| clearCanvas(); | |
| // Set initial volume | |
| audioElement.volume = 0.8; | |
| </script> | |
| </body> | |
| </html> |