| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>Audio Intelligence Demo</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" /> |
| <style> |
| :root { |
| --white: #ffffff; |
| --off-white: #f8fafc; |
| --gray-50: #f1f5f9; |
| --gray-100: #e2e8f0; |
| --gray-200: #cbd5e1; |
| --gray-400: #94a3b8; |
| --gray-600: #475569; |
| --gray-800: #1e293b; |
| --gray-900: #0f172a; |
| --blue-50: #eff6ff; |
| --blue-100: #dbeafe; |
| --blue-400: #60a5fa; |
| --blue-500: #3b82f6; |
| --blue-600: #2563eb; |
| --blue-700: #1d4ed8; |
| --radius-sm: 8px; |
| --radius: 14px; |
| --radius-lg: 20px; |
| --shadow-sm: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04); |
| --shadow: 0 4px 16px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.04); |
| --shadow-lg: 0 20px 48px rgba(0,0,0,0.10), 0 8px 20px rgba(0,0,0,0.06); |
| --shadow-blue: 0 4px 20px rgba(59,130,246,0.25); |
| } |
| |
| * { |
| box-sizing: border-box; |
| margin: 0; |
| padding: 0; |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; |
| } |
| |
| body { |
| background: var(--off-white); |
| color: var(--gray-900); |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| } |
| |
| |
| .icon { |
| width: 18px; |
| height: 18px; |
| stroke: currentColor; |
| stroke-width: 2; |
| stroke-linecap: round; |
| stroke-linejoin: round; |
| fill: none; |
| display: inline-block; |
| vertical-align: middle; |
| flex-shrink: 0; |
| } |
| |
| |
| .hero { |
| width: 100%; |
| max-width: 860px; |
| padding: 3rem 1.5rem 2rem; |
| text-align: center; |
| } |
| |
| .hero-eyebrow { |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| padding: 0.3rem 0.9rem; |
| background: var(--blue-50); |
| color: var(--blue-600); |
| border: 1px solid var(--blue-100); |
| border-radius: 9999px; |
| font-size: 0.78rem; |
| font-weight: 600; |
| letter-spacing: 0.04em; |
| text-transform: uppercase; |
| margin-bottom: 1.25rem; |
| } |
| |
| .hero h1 { |
| font-size: clamp(1.8rem, 4vw, 2.8rem); |
| font-weight: 800; |
| letter-spacing: -0.03em; |
| line-height: 1.15; |
| color: var(--gray-900); |
| margin-bottom: 0.9rem; |
| } |
| |
| .hero h1 span { |
| color: var(--blue-600); |
| } |
| |
| .hero p { |
| font-size: 1rem; |
| color: var(--gray-600); |
| max-width: 540px; |
| margin: 0 auto 2.5rem; |
| line-height: 1.7; |
| font-weight: 400; |
| } |
| |
| |
| .pillars { |
| display: flex; |
| justify-content: center; |
| gap: 0.75rem; |
| flex-wrap: wrap; |
| margin-bottom: 3rem; |
| } |
| |
| .pillar { |
| background: var(--white); |
| border: 1px solid var(--gray-100); |
| border-radius: 10px; |
| padding: 0.5rem 1rem; |
| font-size: 0.82rem; |
| font-weight: 500; |
| color: var(--gray-600); |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| box-shadow: var(--shadow-sm); |
| } |
| |
| .pillar-dot { |
| width: 6px; |
| height: 6px; |
| border-radius: 50%; |
| background: var(--blue-500); |
| } |
| |
| |
| .card { |
| background: var(--white); |
| border: 1px solid var(--gray-100); |
| border-radius: var(--radius-lg); |
| padding: 2rem; |
| box-shadow: var(--shadow-lg); |
| width: 100%; |
| max-width: 860px; |
| margin: 0 1.5rem 2rem; |
| } |
| |
| |
| .demo-section { |
| margin-bottom: 1.75rem; |
| } |
| |
| .demo-grid { |
| display: grid; |
| grid-template-columns: repeat(2, 1fr); |
| gap: 1rem; |
| } |
| |
| @media (max-width: 500px) { |
| .demo-grid { grid-template-columns: 1fr; } |
| } |
| |
| .demo-card { |
| background: var(--blue-50); |
| border: 1px solid var(--blue-100); |
| border-radius: var(--radius); |
| padding: 1rem 1.25rem; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| } |
| |
| .demo-card:hover { |
| border-color: var(--blue-400); |
| background: #e0f2fe; |
| transform: translateY(-2px); |
| box-shadow: var(--shadow); |
| } |
| |
| .demo-card.active { |
| border-color: var(--blue-600); |
| background: #dbeafe; |
| box-shadow: 0 0 0 2px var(--blue-500); |
| } |
| |
| .demo-left { |
| display: flex; |
| align-items: center; |
| gap: 0.85rem; |
| } |
| |
| .demo-icon { |
| width: 38px; |
| height: 38px; |
| background: var(--white); |
| border: 1px solid var(--blue-100); |
| border-radius: 10px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: var(--blue-600); |
| } |
| |
| .demo-title { |
| font-size: 0.9rem; |
| font-weight: 700; |
| color: var(--gray-900); |
| } |
| |
| .demo-sub { |
| font-size: 0.75rem; |
| color: var(--gray-600); |
| } |
| |
| .demo-play-badge { |
| display: flex; |
| align-items: center; |
| gap: 4px; |
| font-size: 0.75rem; |
| font-weight: 700; |
| color: var(--blue-600); |
| background: var(--white); |
| padding: 0.3rem 0.75rem; |
| border-radius: 9999px; |
| border: 1px solid var(--blue-100); |
| } |
| |
| |
| .drop-zone { |
| border: 2px dashed var(--gray-200); |
| border-radius: var(--radius); |
| padding: 1.75rem 1.5rem; |
| text-align: center; |
| background: var(--gray-50); |
| cursor: pointer; |
| transition: all 0.25s ease; |
| margin-bottom: 1.75rem; |
| } |
| |
| .drop-zone:hover, .drop-zone.dragover { |
| border-color: var(--blue-400); |
| background: var(--blue-50); |
| box-shadow: 0 0 0 4px rgba(59,130,246,0.08); |
| } |
| |
| .drop-zone input { |
| display: none; |
| } |
| |
| .drop-zone h3 { |
| font-size: 0.9rem; |
| font-weight: 600; |
| color: var(--gray-800); |
| margin-bottom: 0.25rem; |
| } |
| |
| .drop-zone p { |
| font-size: 0.8rem; |
| color: var(--gray-400); |
| } |
| |
| .btn-upload { |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| margin-top: 0.75rem; |
| padding: 0.45rem 1.1rem; |
| background: var(--blue-600); |
| color: #fff; |
| border: none; |
| border-radius: var(--radius-sm); |
| font-size: 0.82rem; |
| font-weight: 600; |
| cursor: pointer; |
| transition: background 0.2s, box-shadow 0.2s; |
| box-shadow: var(--shadow-blue); |
| } |
| |
| .btn-upload:hover { |
| background: var(--blue-700); |
| box-shadow: 0 6px 24px rgba(59,130,246,0.35); |
| } |
| |
| |
| .action-row { |
| display: flex; |
| gap: 1rem; |
| margin-bottom: 1.75rem; |
| align-items: center; |
| justify-content: space-between; |
| flex-wrap: wrap; |
| } |
| |
| .btn-download { |
| display: inline-flex; |
| align-items: center; |
| gap: 8px; |
| padding: 0.65rem 1.25rem; |
| background: var(--blue-600); |
| color: #fff; |
| border: none; |
| border-radius: var(--radius); |
| font-size: 0.85rem; |
| font-weight: 600; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| box-shadow: var(--shadow-blue); |
| } |
| |
| .btn-download:hover { |
| background: var(--blue-700); |
| box-shadow: 0 6px 24px rgba(59,130,246,0.35); |
| } |
| |
| .btn-download:disabled { |
| background: var(--gray-200); |
| color: var(--gray-400); |
| cursor: not-allowed; |
| box-shadow: none; |
| } |
| |
| |
| .visualizer-section { |
| margin-bottom: 1.75rem; |
| } |
| |
| .section-label { |
| font-size: 0.78rem; |
| font-weight: 600; |
| color: var(--gray-400); |
| letter-spacing: 0.06em; |
| text-transform: uppercase; |
| margin-bottom: 0.6rem; |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| } |
| |
| .visualizer-wrap { |
| background: var(--gray-900); |
| border-radius: var(--radius); |
| overflow: hidden; |
| height: 110px; |
| position: relative; |
| border: 1px solid var(--gray-800); |
| } |
| |
| canvas { |
| width: 100%; |
| height: 100%; |
| display: block; |
| } |
| |
| .vis-idle { |
| position: absolute; |
| inset: 0; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 0.82rem; |
| color: #475569; |
| letter-spacing: 0.03em; |
| } |
| |
| |
| .mode-card { |
| background: var(--blue-50); |
| border: 1px solid var(--blue-100); |
| border-radius: var(--radius); |
| padding: 1.25rem; |
| margin-bottom: 1.75rem; |
| } |
| |
| .mode-header { |
| font-size: 0.95rem; |
| font-weight: 700; |
| color: var(--gray-900); |
| margin-bottom: 0.25rem; |
| } |
| |
| .mode-subtitle { |
| font-size: 0.78rem; |
| color: var(--gray-600); |
| margin-bottom: 1rem; |
| } |
| |
| .segmented-control { |
| display: flex; |
| background: var(--white); |
| border: 1px solid var(--gray-200); |
| border-radius: 10px; |
| padding: 4px; |
| gap: 4px; |
| } |
| |
| .segmented-btn { |
| flex: 1; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 8px; |
| padding: 0.65rem 0.5rem; |
| border: none; |
| background: transparent; |
| border-radius: 7px; |
| font-size: 0.85rem; |
| font-weight: 600; |
| color: var(--gray-600); |
| cursor: pointer; |
| transition: all 0.2s ease; |
| } |
| |
| .segmented-btn.active { |
| background: var(--blue-600); |
| color: var(--white); |
| box-shadow: var(--shadow-blue); |
| } |
| |
| |
| .eq-grid { |
| display: grid; |
| grid-template-columns: repeat(4, 1fr); |
| gap: 1.0rem; |
| margin-bottom: 1.75rem; |
| } |
| |
| @media (max-width: 640px) { |
| .eq-grid { grid-template-columns: repeat(2, 1fr); } |
| } |
| |
| .eq-card { |
| background: var(--gray-50); |
| border: 1px solid var(--gray-100); |
| border-radius: var(--radius); |
| padding: 1.1rem; |
| } |
| |
| .eq-card-label { |
| font-size: 0.78rem; |
| font-weight: 600; |
| color: var(--gray-600); |
| margin-bottom: 0.6rem; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| |
| .eq-value { |
| color: var(--blue-600); |
| font-weight: 700; |
| } |
| |
| .mute-btn { |
| background: none; |
| border: none; |
| cursor: pointer; |
| color: var(--gray-600); |
| padding: 0 2px; |
| display: flex; |
| align-items: center; |
| } |
| |
| input[type="range"] { |
| width: 100%; |
| height: 4px; |
| border-radius: 2px; |
| background: var(--gray-200); |
| outline: none; |
| -webkit-appearance: none; |
| cursor: pointer; |
| } |
| |
| input[type="range"]::-webkit-slider-thumb { |
| -webkit-appearance: none; |
| appearance: none; |
| width: 18px; |
| height: 18px; |
| border-radius: 50%; |
| background: var(--white); |
| border: 2px solid var(--blue-600); |
| cursor: pointer; |
| box-shadow: 0 0 0 3px rgba(59,130,246,0.15), var(--shadow-sm); |
| transition: box-shadow 0.2s; |
| } |
| |
| input[type="range"]::-webkit-slider-thumb:hover { |
| box-shadow: 0 0 0 5px rgba(59,130,246,0.2), var(--shadow-sm); |
| } |
| |
| audio { |
| width: 100%; |
| border-radius: var(--radius-sm); |
| outline: none; |
| accent-color: var(--blue-600); |
| } |
| |
| footer { |
| padding: 1.5rem; |
| text-align: center; |
| font-size: 0.78rem; |
| color: var(--gray-400); |
| letter-spacing: 0.02em; |
| } |
| |
| footer strong { |
| color: var(--gray-600); |
| } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <svg style="display: none;"> |
| <symbol id="icon-bolt" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon> |
| </symbol> |
| <symbol id="icon-flask" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M9 3h6M10 9V3m4 6V3M6 21h12a2 2 0 0 0 2-2v-2L14 9h-4L4 17v2a2 2 0 0 0 2 2z"></path> |
| </symbol> |
| <symbol id="icon-music" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle> |
| </symbol> |
| <symbol id="icon-mic-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <line x1="1" y1="1" x2="23" y2="23"></line> |
| <path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path> |
| <path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path> |
| <line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line> |
| </symbol> |
| <symbol id="icon-play" viewBox="0 0 24 24" fill="currentColor"> |
| <polygon points="5 3 19 12 5 21 5 3"></polygon> |
| </symbol> |
| <symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> |
| <polyline points="7 10 12 15 17 10"></polyline> |
| <line x1="12" y1="15" x2="12" y2="3"></line> |
| </symbol> |
| <symbol id="icon-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path> |
| </symbol> |
| <symbol id="icon-volume" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon> |
| <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path> |
| </symbol> |
| <symbol id="icon-mute" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon> |
| <line x1="23" y1="9" x2="17" y2="15"></line><line x1="17" y1="9" x2="23" y2="15"></line> |
| </symbol> |
| <symbol id="icon-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line> |
| <line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line> |
| <line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line> |
| <line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line> |
| <line x1="17" y1="16" x2="23" y2="16"></line> |
| </symbol> |
| </svg> |
|
|
| |
| <div class="hero"> |
| <div class="hero-eyebrow"> |
| <svg class="icon" style="width:14px;height:14px;"><use href="#icon-flask"></use></svg> |
| Queric Technologies · Proprietary Research |
| </div> |
| <h1>The Future of <span>Audio Intelligence</span></h1> |
| <p> |
| Experience what we've been building. No uploads. No cloud. No waiting. |
| Real-time audio processing running entirely in your browser. |
| </p> |
| <div class="pillars"> |
| <div class="pillar"><span class="pillar-dot"></span>Client-Side Processing</div> |
| <div class="pillar"><span class="pillar-dot"></span>Zero Latency</div> |
| <div class="pillar"><span class="pillar-dot"></span>No Data Uploaded</div> |
| <div class="pillar"><span class="pillar-dot"></span>Realtime Engine</div> |
| </div> |
| <a href="https://doi.org/10.5281/zenodo.21739567"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.21739567.svg" alt="DOI"></a> |
| <br> |
| </div> |
|
|
| |
| <div class="card"> |
|
|
| |
| <div class="demo-section"> |
| <div class="section-label"> |
| <svg class="icon" style="width:14px;height:14px;color:var(--blue-600);"><use href="#icon-bolt"></use></svg> |
| Click to Play Demo Tracks |
| </div> |
| <div class="demo-grid"> |
| <div class="demo-card" id="demo1Card" onclick="loadDemoTrack('1.mp3', 'Demo Song 1')"> |
| <div class="demo-left"> |
| <div class="demo-icon"> |
| <svg class="icon"><use href="#icon-music"></use></svg> |
| </div> |
| <div> |
| <div class="demo-title">Demo Song 1</div> |
| <div class="demo-sub">1.mp3</div> |
| </div> |
| </div> |
| <span class="demo-play-badge"> |
| <svg class="icon" style="width:10px;height:10px;"><use href="#icon-play"></use></svg> |
| Play |
| </span> |
| </div> |
|
|
| <div class="demo-card" id="demo2Card" onclick="loadDemoTrack('2.mp3', 'Demo Song 2')"> |
| <div class="demo-left"> |
| <div class="demo-icon"> |
| <svg class="icon"><use href="#icon-music"></use></svg> |
| </div> |
| <div> |
| <div class="demo-title">Demo Song 2</div> |
| <div class="demo-sub">2.mp3</div> |
| </div> |
| </div> |
| <span class="demo-play-badge"> |
| <svg class="icon" style="width:10px;height:10px;"><use href="#icon-play"></use></svg> |
| Play |
| </span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="drop-zone" id="dropZone"> |
| <h3>Or upload your own audio file</h3> |
| <p>MP3, WAV, FLAC, OGG — processed locally on your device</p> |
| <button class="btn-upload" onclick="document.getElementById('fileInput').click()"> |
| <svg class="icon"><use href="#icon-folder"></use></svg> |
| Browse Custom File |
| </button> |
| <input type="file" id="fileInput" accept="audio/*" /> |
| </div> |
|
|
| |
| <div class="action-row"> |
| <div class="section-label" style="margin:0;"> |
| <svg class="icon" style="width:14px;height:14px;"><use href="#icon-sliders"></use></svg> |
| Live Audio Waveform |
| </div> |
| <button class="btn-download" id="downloadBtn" disabled onclick="downloadProcessedAudio()"> |
| <svg class="icon" id="dlIcon"><use href="#icon-download"></use></svg> |
| <span id="dlText">Download Processed Audio</span> |
| </button> |
| </div> |
|
|
| |
| <div class="visualizer-section"> |
| <div class="visualizer-wrap"> |
| <canvas id="canvas"></canvas> |
| <div class="vis-idle" id="visIdle">Load or click a demo song to view live waveform</div> |
| </div> |
| </div> |
|
|
| |
| <div class="mode-card"> |
| <div class="mode-header">Audio Processing Mode</div> |
| <div class="mode-subtitle">Toggle vocal removal in real-time</div> |
| <div class="segmented-control"> |
| <button class="segmented-btn active" id="btnOriginal" onclick="setAudioMode('original')"> |
| <svg class="icon"><use href="#icon-music"></use></svg> |
| Original Song |
| </button> |
| <button class="segmented-btn" id="btnRemove" onclick="setAudioMode('remove')"> |
| <svg class="icon"><use href="#icon-mic-off"></use></svg> |
| Remove Vocals |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="section-label">Audio Controls & Equalizer</div> |
| <div class="eq-grid"> |
| <div class="eq-card"> |
| <div class="eq-card-label"> |
| <span>Master Volume</span> |
| <button class="mute-btn" id="muteBtn" onclick="toggleMute()"> |
| <svg class="icon" id="muteSvg"><use href="#icon-volume"></use></svg> |
| </button> |
| </div> |
| <input type="range" id="volumeSlider" min="0" max="100" value="100" step="1" /> |
| </div> |
| <div class="eq-card"> |
| <div class="eq-card-label"> |
| <span>Bass · Lows</span> |
| <span class="eq-value" id="bassVal">0 dB</span> |
| </div> |
| <input type="range" id="bassSlider" min="-12" max="12" value="0" step="1" /> |
| </div> |
| <div class="eq-card"> |
| <div class="eq-card-label"> |
| <span>Mid · Vocals</span> |
| <span class="eq-value" id="midVal">0 dB</span> |
| </div> |
| <input type="range" id="midSlider" min="-12" max="12" value="0" step="1" /> |
| </div> |
| <div class="eq-card"> |
| <div class="eq-card-label"> |
| <span>Treble · Highs</span> |
| <span class="eq-value" id="trebleVal">0 dB</span> |
| </div> |
| <input type="range" id="trebleSlider" min="-12" max="12" value="0" step="1" /> |
| </div> |
| </div> |
|
|
| |
| <audio id="audio" controls crossorigin="anonymous"></audio> |
| |
| <div class="mode-subtitle">For the best experience, please use 🎧 headphones.</div> |
| </div> |
|
|
| <footer> |
| © 2026 <strong>Queric Technologies</strong> · All rights reserved · Private Research Demo |
| <br style="margin-bottom: 4px;"> |
| <a href="https://zenodo.org/records/21739567" target="_blank" rel="noopener noreferrer" style="color: var(--gray-400); text-decoration: underline;"> |
| Technical Paper (DOI: 10.5281/zenodo.21739567) |
| </a> |
| </footer> |
|
|
| <script> |
| let ctx, source, analyser, masterGain; |
| let bassFilter, midFilter, trebleFilter; |
| let vocalDryGain, vocalWetGain, vocalBassGain, vocalTrebleGain; |
| let isInitialized = false; |
| let rawAudioArrayBuffer = null; |
| let currentFile = null; |
| let currentMode = "original"; |
| let isMuted = false; |
| let prevVolume = 100; |
| |
| const audio = document.getElementById("audio"); |
| const fileInput = document.getElementById("fileInput"); |
| const dropZone = document.getElementById("dropZone"); |
| const canvas = document.getElementById("canvas"); |
| const visIdle = document.getElementById("visIdle"); |
| const downloadBtn = document.getElementById("downloadBtn"); |
| const dlText = document.getElementById("dlText"); |
| const volumeSlider = document.getElementById("volumeSlider"); |
| const muteSvg = document.getElementById("muteSvg"); |
| const canvasCtx = canvas.getContext("2d"); |
| |
| |
| async function loadDemoTrack(fileName, title) { |
| document.querySelectorAll(".demo-card").forEach(c => c.classList.remove("active")); |
| if (fileName === "1.mp3") document.getElementById("demo1Card").classList.add("active"); |
| if (fileName === "2.mp3") document.getElementById("demo2Card").classList.add("active"); |
| |
| currentFile = { name: title }; |
| |
| await initAudioGraph(); |
| |
| audio.crossOrigin = "anonymous"; |
| audio.src = fileName; |
| |
| try { |
| await audio.play(); |
| } catch (e) { |
| console.warn("Autoplay block:", e); |
| } |
| |
| visIdle.style.display = "none"; |
| downloadBtn.disabled = true; |
| dlText.innerText = "Loading Track..."; |
| |
| try { |
| const res = await fetch(fileName); |
| rawAudioArrayBuffer = await res.arrayBuffer(); |
| downloadBtn.disabled = false; |
| dlText.innerText = "Download Processed Audio"; |
| } catch (err) { |
| console.error("Could not load ArrayBuffer for download", err); |
| dlText.innerText = "Download Processed Audio"; |
| } |
| } |
| |
| |
| dropZone.addEventListener("dragover", (e) => { |
| e.preventDefault(); |
| dropZone.classList.add("dragover"); |
| }); |
| dropZone.addEventListener("dragleave", () => dropZone.classList.remove("dragover")); |
| dropZone.addEventListener("drop", (e) => { |
| e.preventDefault(); |
| dropZone.classList.remove("dragover"); |
| if (e.dataTransfer.files.length) loadCustomFile(e.dataTransfer.files[0]); |
| }); |
| fileInput.addEventListener("change", (e) => { |
| if (e.target.files.length) loadCustomFile(e.target.files[0]); |
| }); |
| |
| async function loadCustomFile(file) { |
| document.querySelectorAll(".demo-card").forEach(c => c.classList.remove("active")); |
| currentFile = file; |
| |
| await initAudioGraph(); |
| |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| rawAudioArrayBuffer = e.target.result; |
| downloadBtn.disabled = false; |
| }; |
| reader.readAsArrayBuffer(file); |
| |
| audio.crossOrigin = "anonymous"; |
| audio.src = URL.createObjectURL(file); |
| try { |
| await audio.play(); |
| } catch (e) { |
| console.warn("Play block:", e); |
| } |
| visIdle.style.display = "none"; |
| } |
| |
| |
| async function initAudioGraph() { |
| if (!ctx) { |
| ctx = new (window.AudioContext || window.webkitAudioContext)(); |
| } |
| if (ctx.state === "suspended") { |
| await ctx.resume(); |
| } |
| |
| if (isInitialized) return; |
| |
| source = ctx.createMediaElementSource(audio); |
| |
| masterGain = ctx.createGain(); |
| masterGain.gain.value = parseFloat(volumeSlider.value) / 100; |
| |
| bassFilter = ctx.createBiquadFilter(); |
| bassFilter.type = "lowshelf"; |
| bassFilter.frequency.value = 100; |
| bassFilter.gain.value = 0; |
| |
| midFilter = ctx.createBiquadFilter(); |
| midFilter.type = "peaking"; |
| midFilter.frequency.value = 1000; |
| midFilter.Q.value = 1; |
| midFilter.gain.value = 0; |
| |
| trebleFilter = ctx.createBiquadFilter(); |
| trebleFilter.type = "highshelf"; |
| trebleFilter.frequency.value = 10000; |
| trebleFilter.gain.value = 0; |
| |
| |
| vocalDryGain = ctx.createGain(); |
| vocalDryGain.gain.value = 1.0; |
| |
| |
| vocalWetGain = ctx.createGain(); |
| vocalWetGain.gain.value = 0.0; |
| |
| vocalBassGain = ctx.createGain(); |
| vocalBassGain.gain.value = 0.0; |
| |
| vocalTrebleGain = ctx.createGain(); |
| vocalTrebleGain.gain.value = 0.0; |
| |
| const splitter = ctx.createChannelSplitter(2); |
| const merger = ctx.createChannelMerger(2); |
| const invertR = ctx.createGain(); |
| invertR.gain.value = -1.0; |
| |
| const notches = [300, 500, 800, 1000, 1300, 1600, 2000, 2400, 2800, 3500].map(freq => { |
| const f = ctx.createBiquadFilter(); |
| f.type = "notch"; |
| f.frequency.value = freq; |
| f.Q.value = 15; |
| return f; |
| }); |
| |
| const haasDelay = ctx.createDelay(0.1); |
| haasDelay.delayTime.value = 0.020; |
| const haasSplitter = ctx.createChannelSplitter(2); |
| const haasMerger = ctx.createChannelMerger(2); |
| |
| const bassLP = ctx.createBiquadFilter(); |
| bassLP.type = "lowpass"; |
| bassLP.frequency.value = 220; |
| |
| const trebleHP = ctx.createBiquadFilter(); |
| trebleHP.type = "highpass"; |
| trebleHP.frequency.value = 8000; |
| |
| analyser = ctx.createAnalyser(); |
| analyser.fftSize = 2048; |
| analyser.smoothingTimeConstant = 0.92; |
| |
| source.connect(bassFilter); |
| bassFilter.connect(midFilter); |
| midFilter.connect(trebleFilter); |
| |
| |
| trebleFilter.connect(vocalDryGain); |
| |
| |
| trebleFilter.connect(splitter); |
| splitter.connect(merger, 0, 0); |
| splitter.connect(invertR, 1); |
| invertR.connect(merger, 0, 0); |
| splitter.connect(merger, 0, 1); |
| invertR.connect(merger, 0, 1); |
| |
| let prev = merger; |
| notches.forEach(n => { prev.connect(n); prev = n; }); |
| prev.connect(vocalWetGain); |
| |
| vocalWetGain.connect(haasSplitter); |
| haasSplitter.connect(haasMerger, 0, 0); |
| haasSplitter.connect(haasDelay, 1); |
| haasDelay.connect(haasMerger, 0, 1); |
| |
| trebleFilter.connect(bassLP); |
| bassLP.connect(vocalBassGain); |
| trebleFilter.connect(trebleHP); |
| trebleHP.connect(vocalTrebleGain); |
| |
| |
| vocalDryGain.connect(masterGain); |
| haasMerger.connect(masterGain); |
| vocalBassGain.connect(masterGain); |
| vocalTrebleGain.connect(masterGain); |
| |
| masterGain.connect(analyser); |
| analyser.connect(ctx.destination); |
| |
| isInitialized = true; |
| drawWaveform(); |
| } |
| |
| function drawWaveform() { |
| requestAnimationFrame(drawWaveform); |
| |
| const W = canvas.width = canvas.clientWidth * window.devicePixelRatio; |
| const H = canvas.height = canvas.clientHeight * window.devicePixelRatio; |
| canvasCtx.scale(1, 1); |
| |
| const bufLen = analyser ? analyser.fftSize : 2048; |
| const data = new Uint8Array(bufLen); |
| if (analyser) analyser.getByteTimeDomainData(data); |
| |
| canvasCtx.fillStyle = "#0f172a"; |
| canvasCtx.fillRect(0, 0, W, H); |
| |
| canvasCtx.strokeStyle = "rgba(255,255,255,0.04)"; |
| canvasCtx.lineWidth = 1; |
| canvasCtx.beginPath(); |
| canvasCtx.moveTo(0, H / 2); |
| canvasCtx.lineTo(W, H / 2); |
| canvasCtx.stroke(); |
| |
| const gradient = canvasCtx.createLinearGradient(0, 0, W, 0); |
| gradient.addColorStop(0, "rgba(96, 165, 250, 0.3)"); |
| gradient.addColorStop(0.2, "rgba(59, 130, 246, 0.9)"); |
| gradient.addColorStop(0.5, "rgba(147, 197, 253, 1)"); |
| gradient.addColorStop(0.8, "rgba(59, 130, 246, 0.9)"); |
| gradient.addColorStop(1, "rgba(96, 165, 250, 0.3)"); |
| |
| canvasCtx.beginPath(); |
| canvasCtx.strokeStyle = gradient; |
| canvasCtx.lineWidth = 2.5 * window.devicePixelRatio; |
| canvasCtx.lineJoin = "round"; |
| canvasCtx.lineCap = "round"; |
| |
| const sliceW = W / bufLen; |
| let x = 0; |
| |
| for (let i = 0; i < bufLen; i++) { |
| const v = (data[i] || 128) / 128.0; |
| const y = (v * H) / 2; |
| if (i === 0) canvasCtx.moveTo(x, y); |
| else canvasCtx.lineTo(x, y); |
| x += sliceW; |
| } |
| canvasCtx.stroke(); |
| } |
| |
| |
| function setAudioMode(mode) { |
| currentMode = mode; |
| document.querySelectorAll(".segmented-btn").forEach(btn => btn.classList.remove("active")); |
| |
| if (mode === "original") { |
| document.getElementById("btnOriginal").classList.add("active"); |
| } else if (mode === "remove") { |
| document.getElementById("btnRemove").classList.add("active"); |
| } |
| |
| if (!isInitialized) return; |
| const now = ctx.currentTime; |
| |
| if (mode === "original") { |
| vocalDryGain.gain.linearRampToValueAtTime(1.0, now + 0.12); |
| vocalWetGain.gain.linearRampToValueAtTime(0.0, now + 0.12); |
| vocalBassGain.gain.linearRampToValueAtTime(0.0, now + 0.12); |
| vocalTrebleGain.gain.linearRampToValueAtTime(0.0, now + 0.12); |
| } else if (mode === "remove") { |
| |
| vocalDryGain.gain.linearRampToValueAtTime(0.0, now + 0.12); |
| vocalWetGain.gain.linearRampToValueAtTime(1.25, now + 0.12); |
| vocalBassGain.gain.linearRampToValueAtTime(0.45, now + 0.12); |
| vocalTrebleGain.gain.linearRampToValueAtTime(0.40, now + 0.12); |
| } |
| } |
| |
| |
| volumeSlider.addEventListener("input", (e) => { |
| const val = parseFloat(e.target.value) / 100; |
| if (masterGain) masterGain.gain.value = val; |
| isMuted = (val === 0); |
| muteSvg.setAttribute("href", isMuted ? "#icon-mute" : "#icon-volume"); |
| }); |
| |
| function toggleMute() { |
| if (isMuted) { |
| volumeSlider.value = prevVolume; |
| if (masterGain) masterGain.gain.value = prevVolume / 100; |
| muteSvg.setAttribute("href", "#icon-volume"); |
| isMuted = false; |
| } else { |
| prevVolume = volumeSlider.value; |
| volumeSlider.value = 0; |
| if (masterGain) masterGain.gain.value = 0; |
| muteSvg.setAttribute("href", "#icon-mute"); |
| isMuted = true; |
| } |
| } |
| |
| |
| document.getElementById("bassSlider").addEventListener("input", (e) => { |
| if (bassFilter) bassFilter.gain.value = e.target.value; |
| document.getElementById("bassVal").innerText = (e.target.value > 0 ? "+" : "") + e.target.value + " dB"; |
| }); |
| document.getElementById("midSlider").addEventListener("input", (e) => { |
| if (midFilter) midFilter.gain.value = e.target.value; |
| document.getElementById("midVal").innerText = (e.target.value > 0 ? "+" : "") + e.target.value + " dB"; |
| }); |
| document.getElementById("trebleSlider").addEventListener("input", (e) => { |
| if (trebleFilter) trebleFilter.gain.value = e.target.value; |
| document.getElementById("trebleVal").innerText = (e.target.value > 0 ? "+" : "") + e.target.value + " dB"; |
| }); |
| |
| |
| async function downloadProcessedAudio() { |
| if (!rawAudioArrayBuffer) return; |
| dlText.innerText = "Rendering Audio..."; |
| downloadBtn.disabled = true; |
| |
| try { |
| const tempCtx = new (window.AudioContext || window.webkitAudioContext)(); |
| const decodedBuffer = await tempCtx.decodeAudioData(rawAudioArrayBuffer.slice(0)); |
| |
| const offlineCtx = new OfflineAudioContext( |
| decodedBuffer.numberOfChannels, |
| decodedBuffer.length, |
| decodedBuffer.sampleRate |
| ); |
| |
| const offSource = offlineCtx.createBufferSource(); |
| offSource.buffer = decodedBuffer; |
| |
| const offBass = offlineCtx.createBiquadFilter(); |
| offBass.type = "lowshelf"; offBass.frequency.value = 100; |
| offBass.gain.value = bassFilter ? bassFilter.gain.value : 0; |
| |
| const offMid = offlineCtx.createBiquadFilter(); |
| offMid.type = "peaking"; offMid.frequency.value = 1000; offMid.Q.value = 1; |
| offMid.gain.value = midFilter ? midFilter.gain.value : 0; |
| |
| const offTreble = offlineCtx.createBiquadFilter(); |
| offTreble.type = "highshelf"; offTreble.frequency.value = 10000; |
| offTreble.gain.value = trebleFilter ? trebleFilter.gain.value : 0; |
| |
| let dryVal = 1.0, wetVal = 0.0, bassVal = 0.0, trebVal = 0.0; |
| if (currentMode === "remove") { |
| dryVal = 0.0; wetVal = 1.25; bassVal = 0.45; trebVal = 0.40; |
| } |
| |
| const offDry = offlineCtx.createGain(); offDry.gain.value = dryVal; |
| const offWet = offlineCtx.createGain(); offWet.gain.value = wetVal; |
| const offBassG = offlineCtx.createGain(); offBassG.gain.value = bassVal; |
| const offTrebG = offlineCtx.createGain(); offTrebG.gain.value = trebVal; |
| |
| const offSplitter = offlineCtx.createChannelSplitter(2); |
| const offMerger = offlineCtx.createChannelMerger(2); |
| const offInvertR = offlineCtx.createGain(); offInvertR.gain.value = -1.0; |
| |
| const offNotches = [300, 500, 800, 1000, 1300, 1600, 2000, 2400, 2800, 3500].map(freq => { |
| const f = offlineCtx.createBiquadFilter(); |
| f.type = "notch"; f.frequency.value = freq; f.Q.value = 15; |
| return f; |
| }); |
| |
| const offHaasDelay = offlineCtx.createDelay(0.1); offHaasDelay.delayTime.value = 0.020; |
| const offHaasSplitter = offlineCtx.createChannelSplitter(2); |
| const offHaasMerger = offlineCtx.createChannelMerger(2); |
| |
| const offBassLP = offlineCtx.createBiquadFilter(); offBassLP.type = "lowpass"; offBassLP.frequency.value = 220; |
| const offTrebleHP = offlineCtx.createBiquadFilter(); offTrebleHP.type = "highpass"; offTrebleHP.frequency.value = 8000; |
| |
| offSource.connect(offBass); |
| offBass.connect(offMid); |
| offMid.connect(offTreble); |
| |
| offTreble.connect(offDry); |
| offTreble.connect(offSplitter); |
| offSplitter.connect(offMerger, 0, 0); |
| offSplitter.connect(offInvertR, 1); |
| offInvertR.connect(offMerger, 0, 0); |
| offSplitter.connect(offMerger, 0, 1); |
| offInvertR.connect(offMerger, 0, 1); |
| |
| let p = offMerger; |
| offNotches.forEach(n => { p.connect(n); p = n; }); |
| p.connect(offWet); |
| |
| offWet.connect(offHaasSplitter); |
| offHaasSplitter.connect(offHaasMerger, 0, 0); |
| offHaasSplitter.connect(offHaasDelay, 1); |
| offHaasDelay.connect(offHaasMerger, 0, 1); |
| |
| offTreble.connect(offBassLP); offBassLP.connect(offBassG); |
| offTreble.connect(offTrebleHP); offTrebleHP.connect(offTrebG); |
| |
| offDry.connect(offlineCtx.destination); |
| offHaasMerger.connect(offlineCtx.destination); |
| offBassG.connect(offlineCtx.destination); |
| offTrebG.connect(offlineCtx.destination); |
| |
| offSource.start(0); |
| const renderedBuffer = await offlineCtx.startRendering(); |
| |
| const wavBlob = audioBufferToWavBlob(renderedBuffer); |
| const url = URL.createObjectURL(wavBlob); |
| const a = document.createElement("a"); |
| a.href = url; |
| a.download = (currentFile ? currentFile.name.replace(/\.[^/.]+$/, "") : "audio") + "_" + currentMode + ".wav"; |
| a.click(); |
| } catch (err) { |
| console.error(err); |
| alert("Failed to render audio for download."); |
| } finally { |
| dlText.innerText = "Download Processed Audio"; |
| downloadBtn.disabled = false; |
| } |
| } |
| |
| function audioBufferToWavBlob(abuffer) { |
| const numOfChan = abuffer.numberOfChannels; |
| const length = abuffer.length * numOfChan * 2 + 44; |
| const buffer = new ArrayBuffer(length); |
| const view = new DataView(buffer); |
| const channels = []; |
| let sample = 0, offset = 0, pos = 0; |
| |
| function setUint32(data) { view.setUint32(pos, data, true); pos += 4; } |
| function setUint16(data) { view.setUint16(pos, data, true); pos += 2; } |
| |
| setUint32(0x46464952); setUint32(length - 8); setUint32(0x45564157); |
| setUint32(0x20746d66); setUint32(16); setUint16(1); setUint16(numOfChan); |
| setUint32(abuffer.sampleRate); setUint32(abuffer.sampleRate * 2 * numOfChan); |
| setUint16(numOfChan * 2); setUint16(16); setUint32(0x61746164); setUint32(length - pos - 4); |
| |
| for (let i = 0; i < numOfChan; i++) channels.push(abuffer.getChannelData(i)); |
| |
| while (pos < length) { |
| for (let i = 0; i < numOfChan; i++) { |
| sample = Math.max(-1, Math.min(1, channels[i][offset])); |
| sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0; |
| view.setInt16(pos, sample, true); |
| pos += 2; |
| } |
| offset++; |
| } |
| return new Blob([buffer], { type: "audio/wav" }); |
| } |
| |
| audio.addEventListener("play", async () => { |
| await initAudioGraph(); |
| }); |
| </script> |
| </body> |
| </html> |
|
|