queric's picture
Update index.html
c2fe7b9 verified
Raw
History Blame Contribute Delete
38.2 kB
<!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;
}
/* SVG Icon Helper */
.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 ─── */
.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;
}
/* ─── Tech Pillars ─── */
.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);
}
/* ─── Main Card ─── */
.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 Tracks Cards ─── */
.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 ─── */
.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 Buttons Row */
.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 ─── */
.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;
}
/* ─── 2-Way Mode Segmented Selector ─── */
.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 & Volume ─── */
.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 Icon Templates -->
<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>
<!-- Hero Section -->
<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>
<!-- Main Demo Card -->
<div class="card">
<!-- Demo Songs Quick Load Section -->
<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>
<!-- Or Drop Custom File -->
<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>
<!-- Action Bar (Download Button) -->
<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>
<!-- Waveform Visualizer -->
<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>
<!-- 2-Way Mode Selector (Original / Remove Vocals) -->
<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>
<!-- Controls Grid (EQ + Master Volume) -->
<div class="section-label">Audio Controls &amp; 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 Controls -->
<audio id="audio" controls crossorigin="anonymous"></audio>
<div class="mode-subtitle">For the best experience, please use 🎧 headphones.</div>
</div>
<footer>
&copy; 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"; // "original" | "remove"
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");
// ─── Demo Song Loader (1.mp3 / 2.mp3) ─────────────────────────────
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";
}
}
// ─── Custom File Drop & Upload ─────────────────────────────────────
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";
}
// ─── Web Audio API Graph Setup ─────────────────────────────────────
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;
// Dry Original Path
vocalDryGain = ctx.createGain();
vocalDryGain.gain.value = 1.0;
// Remove Vocals (Instrumental L-R Path) - Boosted Gain Calibration!
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);
// Dry original path
trebleFilter.connect(vocalDryGain);
// Remove Vocals path
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);
// Mix buses into master gain
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();
}
// ─── Mode Switching (Original / Remove Vocals) ────────────────────
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") {
// BOOSTED VOLUME FOR INSTRUMENTAL (1.25x wet gain)
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);
}
}
// ─── Master Volume Control ─────────────────────────────────────────
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;
}
}
// ─── EQ Sliders ───────────────────────────────────────────────────
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";
});
// ─── Offline Exporter & Downloader ────────────────────────────────
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>