joejoez/claude-code-projects
0
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>RESONANCE — Sound Modifier</title>7 <link rel="preconnect" href="https://fonts.googleapis.com">8 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>9 <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&family=DM+Sans:ital,wght@0,300;0,400;0,500;0,700;1,400&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">10 <style>11 :root {12 --bg-deep: #0a0a0f;13 --bg-surface: #12121a;14 --bg-elevated: #1a1a26;15 --bg-hover: #222233;16 --border-subtle: rgba(255, 255, 255, 0.06);17 --border-accent: rgba(212, 175, 55, 0.3);18 --text-primary: #e8e6e1;19 --text-secondary: #8a8690;20 --text-muted: #5a5660;21 --accent-gold: #d4af37;22 --accent-gold-dim: rgba(212, 175, 55, 0.15);23 --accent-warm: #c9956b;24 --accent-rose: #b5647a;25 --accent-blue: #6b8cc9;26 --success: #5cb885;27 --error: #c95b5b;28 --radius-sm: 6px;29 --radius-md: 12px;30 --radius-lg: 20px;31 --shadow-glow: 0 0 40px rgba(212, 175, 55, 0.08);32 }33 34 *, *::before, *::after {35 box-sizing: border-box;36 margin: 0;37 padding: 0;38 }39 40 html {41 font-size: 16px;42 -webkit-font-smoothing: antialiased;43 -moz-osx-font-smoothing: grayscale;44 }45 46 body {47 font-family: 'DM Sans', sans-serif;48 background: var(--bg-deep);49 color: var(--text-primary);50 min-height: 100vh;51 overflow-x: hidden;52 position: relative;53 }54 55 /* === BACKGROUND ATMOSPHERE === */56 body::before {57 content: '';58 position: fixed;59 top: -50%;60 left: -50%;61 width: 200%;62 height: 200%;63 background:64 radial-gradient(ellipse at 20% 50%, rgba(212, 175, 55, 0.03) 0%, transparent 50%),65 radial-gradient(ellipse at 80% 20%, rgba(107, 140, 201, 0.03) 0%, transparent 50%),66 radial-gradient(ellipse at 50% 80%, rgba(181, 100, 122, 0.02) 0%, transparent 50%);67 pointer-events: none;68 z-index: 0;69 }70 71 /* Geometric grid overlay */72 body::after {73 content: '';74 position: fixed;75 inset: 0;76 background-image:77 linear-gradient(rgba(255,255,255,0.015) 1px, transparent 1px),78 linear-gradient(90deg, rgba(255,255,255,0.015) 1px, transparent 1px);79 background-size: 60px 60px;80 pointer-events: none;81 z-index: 0;82 }83 84 /* === LAYOUT === */85 .app {86 position: relative;87 z-index: 1;88 max-width: 860px;89 margin: 0 auto;90 padding: 3rem 2rem 4rem;91 }92 93 /* === HEADER === */94 .header {95 text-align: center;96 margin-bottom: 3.5rem;97 animation: fadeInDown 0.8s ease-out;98 }99 100 .header__eyebrow {101 font-family: 'JetBrains Mono', monospace;102 font-size: 0.7rem;103 font-weight: 300;104 letter-spacing: 0.35em;105 text-transform: uppercase;106 color: var(--accent-gold);107 margin-bottom: 1rem;108 display: flex;109 align-items: center;110 justify-content: center;111 gap: 1rem;112 }113 114 .header__eyebrow::before,115 .header__eyebrow::after {116 content: '';117 width: 40px;118 height: 1px;119 background: linear-gradient(90deg, transparent, var(--accent-gold), transparent);120 }121 122 .header__title {123 font-family: 'Playfair Display', serif;124 font-size: 3.5rem;125 font-weight: 900;126 letter-spacing: -0.02em;127 line-height: 1;128 background: linear-gradient(135deg, var(--text-primary) 0%, var(--accent-gold) 50%, var(--accent-warm) 100%);129 -webkit-background-clip: text;130 -webkit-text-fill-color: transparent;131 background-clip: text;132 margin-bottom: 0.75rem;133 }134 135 .header__subtitle {136 font-size: 0.95rem;137 color: var(--text-secondary);138 font-weight: 300;139 letter-spacing: 0.02em;140 }141 142 /* === CARD === */143 .card {144 background: var(--bg-surface);145 border: 1px solid var(--border-subtle);146 border-radius: var(--radius-lg);147 padding: 2.5rem;148 margin-bottom: 1.5rem;149 position: relative;150 overflow: hidden;151 animation: fadeInUp 0.6s ease-out;152 animation-fill-mode: both;153 }154 155 .card:nth-child(2) { animation-delay: 0.1s; }156 .card:nth-child(3) { animation-delay: 0.2s; }157 .card:nth-child(4) { animation-delay: 0.3s; }158 159 .card::before {160 content: '';161 position: absolute;162 top: 0;163 left: 0;164 right: 0;165 height: 1px;166 background: linear-gradient(90deg, transparent, var(--accent-gold-dim), transparent);167 }168 169 .card__title {170 font-family: 'Playfair Display', serif;171 font-size: 1.25rem;172 font-weight: 700;173 margin-bottom: 1.75rem;174 display: flex;175 align-items: center;176 gap: 0.75rem;177 }178 179 .card__title-icon {180 width: 28px;181 height: 28px;182 border-radius: 8px;183 display: flex;184 align-items: center;185 justify-content: center;186 font-size: 0.85rem;187 flex-shrink: 0;188 }189 190 .card__title-icon--gold { background: var(--accent-gold-dim); color: var(--accent-gold); }191 .card__title-icon--blue { background: rgba(107,140,201,0.15); color: var(--accent-blue); }192 .card__title-icon--rose { background: rgba(181,100,122,0.15); color: var(--accent-rose); }193 .card__title-icon--warm { background: rgba(201,149,107,0.15); color: var(--accent-warm); }194 195 /* === UPLOAD ZONE === */196 .upload-zone {197 border: 2px dashed var(--border-subtle);198 border-radius: var(--radius-md);199 padding: 3rem 2rem;200 text-align: center;201 cursor: pointer;202 transition: all 0.3s ease;203 position: relative;204 overflow: hidden;205 }206 207 .upload-zone:hover {208 border-color: var(--accent-gold);209 background: var(--accent-gold-dim);210 }211 212 .upload-zone.drag-over {213 border-color: var(--accent-gold);214 background: var(--accent-gold-dim);215 transform: scale(1.01);216 }217 218 .upload-zone.has-file {219 border-color: var(--success);220 border-style: solid;221 background: rgba(92, 184, 133, 0.06);222 }223 224 .upload-zone__icon {225 font-size: 2.5rem;226 margin-bottom: 0.75rem;227 opacity: 0.6;228 }229 230 .upload-zone__text {231 font-size: 0.95rem;232 color: var(--text-secondary);233 margin-bottom: 0.25rem;234 }235 236 .upload-zone__hint {237 font-size: 0.75rem;238 color: var(--text-muted);239 font-family: 'JetBrains Mono', monospace;240 }241 242 .upload-zone__filename {243 font-family: 'JetBrains Mono', monospace;244 font-size: 0.85rem;245 color: var(--success);246 margin-top: 0.5rem;247 word-break: break-all;248 }249 250 .upload-zone input[type="file"] {251 display: none;252 }253 254 /* === CONTROLS === */255 .control-group {256 margin-bottom: 1.75rem;257 }258 259 .control-group:last-child {260 margin-bottom: 0;261 }262 263 .control-label {264 font-family: 'JetBrains Mono', monospace;265 font-size: 0.7rem;266 font-weight: 500;267 letter-spacing: 0.15em;268 text-transform: uppercase;269 color: var(--text-muted);270 margin-bottom: 0.75rem;271 display: block;272 }273 274 /* === PITCH SELECTOR === */275 .pitch-grid {276 display: grid;277 grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));278 gap: 6px;279 }280 281 .pitch-btn {282 background: var(--bg-elevated);283 border: 1px solid var(--border-subtle);284 border-radius: var(--radius-sm);285 padding: 0.6rem 0.25rem;286 color: var(--text-secondary);287 font-family: 'JetBrains Mono', monospace;288 font-size: 0.75rem;289 cursor: pointer;290 transition: all 0.2s ease;291 text-align: center;292 }293 294 .pitch-btn:hover {295 border-color: var(--accent-gold);296 color: var(--text-primary);297 background: var(--bg-hover);298 }299 300 .pitch-btn.active {301 background: var(--accent-gold-dim);302 border-color: var(--accent-gold);303 color: var(--accent-gold);304 font-weight: 500;305 }306 307 .pitch-btn .note-name {308 display: block;309 font-size: 0.65rem;310 color: var(--text-muted);311 margin-top: 2px;312 }313 314 .pitch-btn.active .note-name {315 color: var(--accent-warm);316 }317 318 /* === SPEED SLIDER === */319 .speed-control {320 display: flex;321 align-items: center;322 gap: 1.25rem;323 }324 325 .speed-slider-wrap {326 flex: 1;327 position: relative;328 }329 330 input[type="range"] {331 -webkit-appearance: none;332 width: 100%;333 height: 4px;334 background: var(--bg-elevated);335 border-radius: 2px;336 outline: none;337 cursor: pointer;338 }339 340 input[type="range"]::-webkit-slider-thumb {341 -webkit-appearance: none;342 width: 18px;343 height: 18px;344 background: var(--accent-gold);345 border-radius: 50%;346 cursor: pointer;347 box-shadow: 0 0 12px rgba(212, 175, 55, 0.3);348 transition: transform 0.15s ease;349 }350 351 input[type="range"]::-webkit-slider-thumb:hover {352 transform: scale(1.2);353 }354 355 input[type="range"]::-moz-range-thumb {356 width: 18px;357 height: 18px;358 background: var(--accent-gold);359 border-radius: 50%;360 cursor: pointer;361 border: none;362 box-shadow: 0 0 12px rgba(212, 175, 55, 0.3);363 }364 365 .speed-value {366 font-family: 'JetBrains Mono', monospace;367 font-size: 1.1rem;368 font-weight: 500;369 color: var(--accent-gold);370 min-width: 52px;371 text-align: right;372 }373 374 .speed-markers {375 display: flex;376 justify-content: space-between;377 margin-top: 6px;378 font-family: 'JetBrains Mono', monospace;379 font-size: 0.6rem;380 color: var(--text-muted);381 }382 383 /* === OUTPUT MODE TOGGLE === */384 .mode-toggle {385 display: grid;386 grid-template-columns: repeat(3, 1fr);387 gap: 8px;388 }389 390 .mode-btn {391 background: var(--bg-elevated);392 border: 1px solid var(--border-subtle);393 border-radius: var(--radius-md);394 padding: 1rem 0.75rem;395 color: var(--text-secondary);396 cursor: pointer;397 transition: all 0.25s ease;398 text-align: center;399 }400 401 .mode-btn:hover {402 border-color: var(--accent-gold);403 background: var(--bg-hover);404 }405 406 .mode-btn.active {407 background: var(--accent-gold-dim);408 border-color: var(--accent-gold);409 color: var(--accent-gold);410 }411 412 .mode-btn__icon {413 font-size: 1.5rem;414 margin-bottom: 0.4rem;415 }416 417 .mode-btn__label {418 font-size: 0.8rem;419 font-weight: 500;420 display: block;421 }422 423 .mode-btn__desc {424 font-size: 0.65rem;425 color: var(--text-muted);426 margin-top: 0.2rem;427 display: block;428 font-family: 'JetBrains Mono', monospace;429 }430 431 .mode-btn.active .mode-btn__desc {432 color: var(--accent-warm);433 }434 435 /* === FORMAT TOGGLE === */436 .format-toggle {437 display: flex;438 gap: 8px;439 margin-top: 1.75rem;440 }441 442 .format-btn {443 flex: 1;444 background: var(--bg-elevated);445 border: 1px solid var(--border-subtle);446 border-radius: var(--radius-sm);447 padding: 0.65rem 1rem;448 color: var(--text-secondary);449 font-family: 'JetBrains Mono', monospace;450 font-size: 0.8rem;451 cursor: pointer;452 transition: all 0.2s ease;453 text-align: center;454 }455 456 .format-btn:hover {457 border-color: var(--accent-gold);458 background: var(--bg-hover);459 }460 461 .format-btn.active {462 background: var(--accent-gold-dim);463 border-color: var(--accent-gold);464 color: var(--accent-gold);465 font-weight: 500;466 }467 468 /* === PROCESS BUTTON === */469 .process-btn {470 width: 100%;471 padding: 1.1rem 2rem;472 background: linear-gradient(135deg, var(--accent-gold), var(--accent-warm));473 border: none;474 border-radius: var(--radius-md);475 color: var(--bg-deep);476 font-family: 'Playfair Display', serif;477 font-size: 1.1rem;478 font-weight: 700;479 letter-spacing: 0.05em;480 cursor: pointer;481 transition: all 0.3s ease;482 position: relative;483 overflow: hidden;484 }485 486 .process-btn::before {487 content: '';488 position: absolute;489 inset: 0;490 background: linear-gradient(135deg, transparent, rgba(255,255,255,0.15), transparent);491 transform: translateX(-100%);492 transition: transform 0.6s ease;493 }494 495 .process-btn:hover::before {496 transform: translateX(100%);497 }498 499 .process-btn:hover {500 transform: translateY(-1px);501 box-shadow: 0 8px 30px rgba(212, 175, 55, 0.25);502 }503 504 .process-btn:disabled {505 opacity: 0.4;506 cursor: not-allowed;507 transform: none;508 box-shadow: none;509 }510 511 .process-btn:disabled::before {512 display: none;513 }514 515 /* === STATUS / PROGRESS === */516 .status-bar {517 display: none;518 margin-top: 1.5rem;519 padding: 1.25rem;520 background: var(--bg-elevated);521 border-radius: var(--radius-md);522 border: 1px solid var(--border-subtle);523 }524 525 .status-bar.visible {526 display: block;527 animation: fadeInUp 0.3s ease-out;528 }529 530 .status-bar__text {531 font-family: 'JetBrains Mono', monospace;532 font-size: 0.8rem;533 color: var(--text-secondary);534 margin-bottom: 0.75rem;535 display: flex;536 align-items: center;537 gap: 0.5rem;538 }539 540 .status-bar__text .spinner {541 display: inline-block;542 width: 14px;543 height: 14px;544 border: 2px solid var(--accent-gold-dim);545 border-top-color: var(--accent-gold);546 border-radius: 50%;547 animation: spin 0.8s linear infinite;548 }549 550 .progress-track {551 height: 3px;552 background: var(--bg-deep);553 border-radius: 2px;554 overflow: hidden;555 }556 557 .progress-fill {558 height: 100%;559 background: linear-gradient(90deg, var(--accent-gold), var(--accent-warm));560 border-radius: 2px;561 width: 0%;562 transition: width 0.3s ease;563 animation: progressPulse 2s ease-in-out infinite;564 }565 566 .status-bar.completed .progress-fill {567 animation: none;568 width: 100%;569 background: var(--success);570 }571 572 .status-bar.error {573 border-color: rgba(201, 91, 91, 0.3);574 }575 576 .status-bar.error .status-bar__text {577 color: var(--error);578 }579 580 /* === DOWNLOAD BUTTON === */581 .download-btn {582 display: none;583 width: 100%;584 margin-top: 1rem;585 padding: 0.9rem 2rem;586 background: transparent;587 border: 1px solid var(--success);588 border-radius: var(--radius-md);589 color: var(--success);590 font-family: 'DM Sans', sans-serif;591 font-size: 0.9rem;592 font-weight: 500;593 cursor: pointer;594 transition: all 0.25s ease;595 }596 597 .download-btn.visible {598 display: block;599 animation: fadeInUp 0.3s ease-out;600 }601 602 .download-btn:hover {603 background: rgba(92, 184, 133, 0.1);604 transform: translateY(-1px);605 }606 607 /* === DOWNLOAD LINK === */608 .download-link {609 display: none;610 margin-top: 0.75rem;611 padding: 0.65rem 1rem;612 background: var(--bg-elevated);613 border: 1px solid var(--border-subtle);614 border-radius: var(--radius-sm);615 font-family: 'JetBrains Mono', monospace;616 font-size: 0.7rem;617 color: var(--text-secondary);618 word-break: break-all;619 position: relative;620 }621 622 .download-link.visible {623 display: block;624 animation: fadeInUp 0.3s ease-out;625 }626 627 .download-link__label {628 font-size: 0.6rem;629 letter-spacing: 0.1em;630 text-transform: uppercase;631 color: var(--text-muted);632 margin-bottom: 0.4rem;633 }634 635 .download-link__url {636 display: flex;637 align-items: center;638 gap: 0.5rem;639 }640 641 .download-link__text {642 flex: 1;643 overflow: hidden;644 text-overflow: ellipsis;645 color: var(--accent-blue);646 }647 648 .download-link__copy {649 flex-shrink: 0;650 background: var(--bg-hover);651 border: 1px solid var(--border-subtle);652 border-radius: var(--radius-sm);653 padding: 0.3rem 0.6rem;654 color: var(--text-secondary);655 font-family: 'JetBrains Mono', monospace;656 font-size: 0.65rem;657 cursor: pointer;658 transition: all 0.2s ease;659 }660 661 .download-link__copy:hover {662 border-color: var(--accent-gold);663 color: var(--accent-gold);664 }665 666 /* === FOOTER === */667 .footer {668 text-align: center;669 margin-top: 3rem;670 padding-top: 2rem;671 border-top: 1px solid var(--border-subtle);672 animation: fadeInUp 0.6s ease-out 0.4s both;673 }674 675 .footer__text {676 font-family: 'JetBrains Mono', monospace;677 font-size: 0.65rem;678 color: var(--text-muted);679 letter-spacing: 0.1em;680 }681 682 .footer__tech {683 margin-top: 0.5rem;684 display: flex;685 justify-content: center;686 gap: 1.5rem;687 font-family: 'JetBrains Mono', monospace;688 font-size: 0.6rem;689 color: var(--text-muted);690 }691 692 .footer__tech span {693 opacity: 0.5;694 }695 696 /* === ANIMATIONS === */697 @keyframes fadeInDown {698 from { opacity: 0; transform: translateY(-20px); }699 to { opacity: 1; transform: translateY(0); }700 }701 702 @keyframes fadeInUp {703 from { opacity: 0; transform: translateY(16px); }704 to { opacity: 1; transform: translateY(0); }705 }706 707 @keyframes spin {708 to { transform: rotate(360deg); }709 }710 711 @keyframes progressPulse {712 0%, 100% { opacity: 1; }713 50% { opacity: 0.7; }714 }715 716 /* === RESPONSIVE === */717 @media (max-width: 640px) {718 .app { padding: 2rem 1.25rem 3rem; }719 .header__title { font-size: 2.5rem; }720 .card { padding: 1.75rem; }721 .pitch-grid { grid-template-columns: repeat(auto-fill, minmax(44px, 1fr)); }722 .mode-toggle { grid-template-columns: 1fr; }723 }724 </style>725</head>726<body>727 <div class="app">728 <!-- Header -->729 <header class="header">730 <div class="header__eyebrow">Sound Modifier</div>731 <h1 class="header__title">RESONANCE</h1>732 <p class="header__subtitle">Separate vocals, shift pitch, adjust tempo — all locally on your machine.</p>733 </header>734 735 <!-- Upload Card -->736 <div class="card">737 <h2 class="card__title">738 <span class="card__title-icon card__title-icon--gold">♫</span>739 Source Audio740 </h2>741 <div class="upload-zone" id="uploadZone">742 <div class="upload-zone__icon">💾</div>743 <div class="upload-zone__text">Drop your audio file here or click to browse</div>744 <div class="upload-zone__hint">WAV · MP3 · FLAC · OGG · M4A</div>745 <div class="upload-zone__filename" id="fileName"></div>746 <input type="file" id="fileInput" accept="audio/*">747 </div>748 </div>749 750 <!-- Output Mode Card -->751 <div class="card">752 <h2 class="card__title">753 <span class="card__title-icon card__title-icon--rose">☷</span>754 Output Mode755 </h2>756 <div class="mode-toggle">757 <button class="mode-btn" data-mode="vocals" onclick="setMode('vocals')">758 <span class="mode-btn__icon">🎤</span>759 <span class="mode-btn__label">Vocals Only</span>760 <span class="mode-btn__desc">isolated voice</span>761 </button>762 <button class="mode-btn" data-mode="music" onclick="setMode('music')">763 <span class="mode-btn__icon">🎹</span>764 <span class="mode-btn__label">Music Only</span>765 <span class="mode-btn__desc">instrumental</span>766 </button>767 <button class="mode-btn active" data-mode="both" onclick="setMode('both')">768 <span class="mode-btn__icon">🎶</span>769 <span class="mode-btn__label">Full Mix</span>770 <span class="mode-btn__desc">vocals + music</span>771 </button>772 </div>773 774 <div class="control-group" style="margin-top: 1.75rem;">775 <label class="control-label">Output Format</label>776 <div class="format-toggle">777 <button class="format-btn active" data-format="mp3" onclick="setFormat('mp3')">MP3</button>778 <button class="format-btn" data-format="wav" onclick="setFormat('wav')">WAV</button>779 </div>780 </div>781 </div>782 783 <!-- Pitch & Speed Card -->784 <div class="card">785 <h2 class="card__title">786 <span class="card__title-icon card__title-icon--blue">♮</span>787 Pitch & Tempo788 </h2>789 790 <div class="control-group">791 <label class="control-label">Pitch Shift (semitones)</label>792 <div class="pitch-grid" id="pitchGrid"></div>793 </div>794 795 <div class="control-group">796 <label class="control-label">Playback Speed</label>797 <div class="speed-control">798 <div class="speed-slider-wrap">799 <input type="range" id="speedSlider" min="0.25" max="2.0" step="0.05" value="1.0">800 <div class="speed-markers">801 <span>0.25×</span>802 <span>1.0×</span>803 <span>2.0×</span>804 </div>805 </div>806 <div class="speed-value" id="speedValue">1.00×</div>807 </div>808 </div>809 </div>810 811 <!-- Process Card -->812 <div class="card">813 <h2 class="card__title">814 <span class="card__title-icon card__title-icon--warm">⚙</span>815 Process816 </h2>817 <button class="process-btn" id="processBtn" disabled onclick="processAudio()">818 Transform Audio819 </button>820 <div class="status-bar" id="statusBar">821 <div class="status-bar__text" id="statusText">822 <span class="spinner"></span>823 <span>Processing...</span>824 </div>825 <div class="progress-track">826 <div class="progress-fill" id="progressFill"></div>827 </div>828 </div>829 <button class="download-btn" id="downloadBtn" onclick="downloadResult()">830 ↓ Download Modified Audio831 </button>832 <div class="download-link" id="downloadLink">833 <div class="download-link__label">Direct link (right-click to copy)</div>834 <div class="download-link__url">835 <a class="download-link__text" id="downloadLinkUrl" href="#" target="_blank"></a>836 <button class="download-link__copy" id="copyLinkBtn" onclick="copyDownloadLink()">Copy</button>837 </div>838 </div>839 </div>840 841 <!-- Footer -->842 <footer class="footer">843 <div class="footer__text">RESONANCE — LOCAL AUDIO PROCESSING</div>844 <div class="footer__tech">845 <span>DEMUCS</span>846 <span>LIBROSA</span>847 <span>FASTAPI</span>848 </div>849 </footer>850 </div>851 852 <script>853 // === STATE ===854 let selectedFile = null;855 let outputMode = 'both';856 let pitchSemitones = 0;857 let speedFactor = 1.0;858 let outputFormat = 'mp3';859 let currentJobId = null;860 let pollInterval = null;861 862 // === PITCH GRID ===863 const pitchNames = {864 '-12': '-12', '-11': '-11', '-10': '-10', '-9': '-9', '-8': '-8',865 '-7': '-7', '-6': '-6', '-5': '-5', '-4': '-4', '-3': '-3',866 '-2': '-2', '-1': '-1', '0': '0', '1': '+1', '2': '+2',867 '3': '+3', '4': '+4', '5': '+5', '6': '+6', '7': '+7',868 '8': '+8', '9': '+9', '10': '+10', '11': '+11', '12': '+12'869 };870 871 const noteLabels = {872 '-12': '-8va', '-7': '-P5', '-5': '-P4', '-3': 'm3↓', '-2': 'M2↓',873 '-1': 'm2↓', '0': '—', '1': 'm2↑', '2': 'M2↑', '3': 'm3↑',874 '5': 'P4↑', '7': 'P5↑', '12': '+8va'875 };876 877 function buildPitchGrid() {878 const grid = document.getElementById('pitchGrid');879 for (let i = -12; i <= 12; i++) {880 const btn = document.createElement('button');881 btn.className = 'pitch-btn' + (i === 0 ? ' active' : '');882 btn.dataset.pitch = i;883 btn.innerHTML = `${i > 0 ? '+' : ''}${i}` +884 (noteLabels[String(i)] ? `<span class="note-name">${noteLabels[String(i)]}</span>` : '');885 btn.addEventListener('click', () => setPitch(i));886 grid.appendChild(btn);887 }888 }889 890 function setPitch(semitones) {891 pitchSemitones = semitones;892 document.querySelectorAll('.pitch-btn').forEach(btn => {893 btn.classList.toggle('active', parseInt(btn.dataset.pitch) === semitones);894 });895 }896 897 // === MODE TOGGLE ===898 function setMode(mode) {899 outputMode = mode;900 document.querySelectorAll('.mode-btn').forEach(btn => {901 btn.classList.toggle('active', btn.dataset.mode === mode);902 });903 }904 905 // === FORMAT TOGGLE ===906 function setFormat(format) {907 outputFormat = format;908 document.querySelectorAll('.format-btn').forEach(btn => {909 btn.classList.toggle('active', btn.dataset.format === format);910 });911 }912 913 // === SPEED SLIDER ===914 const speedSlider = document.getElementById('speedSlider');915 const speedValue = document.getElementById('speedValue');916 speedSlider.addEventListener('input', () => {917 speedFactor = parseFloat(speedSlider.value);918 speedValue.innerHTML = speedFactor.toFixed(2) + '×';919 });920 921 // === FILE UPLOAD ===922 const uploadZone = document.getElementById('uploadZone');923 const fileInput = document.getElementById('fileInput');924 const fileNameEl = document.getElementById('fileName');925 const processBtn = document.getElementById('processBtn');926 927 uploadZone.addEventListener('click', () => fileInput.click());928 929 uploadZone.addEventListener('dragover', (e) => {930 e.preventDefault();931 uploadZone.classList.add('drag-over');932 });933 934 uploadZone.addEventListener('dragleave', () => {935 uploadZone.classList.remove('drag-over');936 });937 938 uploadZone.addEventListener('drop', (e) => {939 e.preventDefault();940 uploadZone.classList.remove('drag-over');941 if (e.dataTransfer.files.length > 0) {942 handleFile(e.dataTransfer.files[0]);943 }944 });945 946 fileInput.addEventListener('change', () => {947 if (fileInput.files.length > 0) {948 handleFile(fileInput.files[0]);949 }950 });951 952 function handleFile(file) {953 selectedFile = file;954 uploadZone.classList.add('has-file');955 fileNameEl.textContent = file.name + ' (' + formatSize(file.size) + ')';956 processBtn.disabled = false;957 }958 959 function formatSize(bytes) {960 if (bytes < 1024) return bytes + ' B';961 if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';962 return (bytes / (1024 * 1024)).toFixed(1) + ' MB';963 }964 965 // === PROCESS AUDIO ===966 async function processAudio() {967 if (!selectedFile) return;968 969 const statusBar = document.getElementById('statusBar');970 const statusText = document.getElementById('statusText');971 const progressFill = document.getElementById('progressFill');972 const downloadBtn = document.getElementById('downloadBtn');973 const downloadLink = document.getElementById('downloadLink');974 975 processBtn.disabled = true;976 downloadBtn.classList.remove('visible');977 downloadLink.classList.remove('visible');978 statusBar.className = 'status-bar visible';979 statusText.innerHTML = '<span class="spinner"></span><span>Uploading file...</span>';980 progressFill.style.width = '5%';981 982 try {983 const formData = new FormData();984 formData.append('file', selectedFile);985 formData.append('output_mode', outputMode);986 formData.append('pitch_semitones', pitchSemitones.toString());987 formData.append('speed_factor', speedFactor.toString());988 formData.append('output_format', outputFormat);989 990 const response = await fetch('/api/process', {991 method: 'POST',992 body: formData,993 });994 995 if (!response.ok) {996 const err = await response.json();997 throw new Error(err.detail || 'Upload failed');998 }999 1000 const data = await response.json();1001 currentJobId = data.job_id;1002 progressFill.style.width = '15%';1003 statusText.innerHTML = '<span class="spinner"></span><span>Processing queued...</span>';1004 1005 // Poll for completion1006 pollForCompletion();1007 1008 } catch (err) {1009 statusBar.className = 'status-bar visible error';1010 statusText.innerHTML = '✗ Error: ' + err.message;1011 processBtn.disabled = false;1012 }1013 }1014 1015 function pollForCompletion() {1016 const statusText = document.getElementById('statusText');1017 const progressFill = document.getElementById('progressFill');1018 const statusBar = document.getElementById('statusBar');1019 const downloadBtn = document.getElementById('downloadBtn');1020 const downloadLink = document.getElementById('downloadLink');1021 const downloadLinkUrl = document.getElementById('downloadLinkUrl');1022 let progress = 15;1023 1024 const messages = [1025 'Separating stems with Demucs AI...',1026 'Applying pitch transformation...',1027 'Adjusting tempo...',1028 'Encoding output...',1029 ];1030 let msgIdx = 0;1031 1032 pollInterval = setInterval(async () => {1033 try {1034 const res = await fetch(`/api/status/${currentJobId}`);1035 if (!res.ok) {1036 // Server might be restarting, keep polling1037 return;1038 }1039 const text = await res.text();1040 let data;1041 try { data = JSON.parse(text); } catch { return; } // Non-JSON response, retry1042 1043 if (data.status === 'completed') {1044 clearInterval(pollInterval);1045 progressFill.style.width = '100%';1046 statusBar.className = 'status-bar visible completed';1047 statusText.innerHTML = '✓ Processing complete!';1048 downloadBtn.classList.add('visible');1049 const dlUrl = `${window.location.origin}/api/download/${currentJobId}`;1050 downloadLinkUrl.href = dlUrl;1051 downloadLinkUrl.textContent = dlUrl;1052 downloadLink.classList.add('visible');1053 processBtn.disabled = false;1054 return;1055 }1056 1057 if (data.status === 'error') {1058 clearInterval(pollInterval);1059 statusBar.className = 'status-bar visible error';1060 statusText.innerHTML = '✗ Error: ' + (data.error || 'Unknown error');1061 processBtn.disabled = false;1062 return;1063 }1064 1065 progress = Math.min(progress + 2, 92);1066 progressFill.style.width = progress + '%';1067 msgIdx = Math.min(Math.floor((progress - 15) / 20), messages.length - 1);1068 statusText.innerHTML = '<span class="spinner"></span><span>' + messages[msgIdx] + '</span>';1069 1070 } catch {1071 // Transient network error — keep polling1072 }1073 }, 2500);1074 }1075 1076 // === DOWNLOAD ===1077 async function downloadResult() {1078 if (!currentJobId) return;1079 const downloadBtn = document.getElementById('downloadBtn');1080 downloadBtn.textContent = '⏳ Preparing download...';1081 downloadBtn.disabled = true;1082 try {1083 const resp = await fetch(`/api/download/${currentJobId}`);1084 if (!resp.ok) throw new Error('Download failed');1085 const blob = await resp.blob();1086 const disposition = resp.headers.get('content-disposition') || '';1087 const match = disposition.match(/filename="?([^"]+)"?/);1088 const filename = match ? match[1] : `modified_audio.${outputFormat}`;1089 const url = URL.createObjectURL(blob);1090 const a = document.createElement('a');1091 a.href = url;1092 a.download = filename;1093 document.body.appendChild(a);1094 a.click();1095 document.body.removeChild(a);1096 URL.revokeObjectURL(url);1097 } catch (err) {1098 alert('Download failed: ' + err.message);1099 } finally {1100 downloadBtn.textContent = '↓ Download Modified Audio';1101 downloadBtn.disabled = false;1102 }1103 }1104 1105 // === COPY LINK ===1106 async function copyDownloadLink() {1107 const url = document.getElementById('downloadLinkUrl').href;1108 const btn = document.getElementById('copyLinkBtn');1109 try {1110 await navigator.clipboard.writeText(url);1111 btn.textContent = 'Copied!';1112 setTimeout(() => { btn.textContent = 'Copy'; }, 2000);1113 } catch {1114 // Fallback for iframe/older browsers1115 const input = document.createElement('input');1116 input.value = url;1117 document.body.appendChild(input);1118 input.select();1119 document.execCommand('copy');1120 document.body.removeChild(input);1121 btn.textContent = 'Copied!';1122 setTimeout(() => { btn.textContent = 'Copy'; }, 2000);1123 }1124 }1125 1126 // === INIT ===1127 buildPitchGrid();1128 </script>1129</body>1130</html>1131 