Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PitchLab Pro - DAW-Integrated Audio Pitch Editor</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --bg-primary: #0f0f0f; | |
| --bg-secondary: #1a1a1a; | |
| --bg-tertiary: #252525; | |
| --bg-hover: #2e2e2e; | |
| --text-primary: #ffffff; | |
| --text-secondary: #a0a0a0; | |
| --accent-blue: #3b82f6; | |
| --accent-purple: #8b5cf6; | |
| --accent-green: #10b981; | |
| --accent-orange: #f59e0b; | |
| --accent-red: #ef4444; | |
| --accent-yellow: #eab308; | |
| --border-color: #333333; | |
| --grid-color: #1e1e1e; | |
| --success-green: #22c55e; | |
| --warning-yellow: #fbbf24; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Header */ | |
| .header { | |
| background: linear-gradient(180deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%); | |
| border-bottom: 1px solid var(--border-color); | |
| padding: 10px 20px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-shrink: 0; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.3); | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| font-size: 22px; | |
| font-weight: 600; | |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .logo-icon { | |
| width: 36px; | |
| height: 36px; | |
| background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue)); | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 20px; | |
| } | |
| .header-controls { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| } | |
| .header-btn { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-color); | |
| color: var(--text-secondary); | |
| padding: 8px 16px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| font-size: 13px; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .header-btn:hover { | |
| background: var(--bg-hover); | |
| color: var(--text-primary); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| } | |
| /* Toolbar */ | |
| .toolbar { | |
| background: var(--bg-secondary); | |
| border-bottom: 1px solid var(--border-color); | |
| padding: 10px 20px; | |
| display: flex; | |
| gap: 20px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.2); | |
| } | |
| .tool-group { | |
| display: flex; | |
| gap: 8px; | |
| padding: 0 12px; | |
| border-right: 1px solid var(--border-color); | |
| } | |
| .tool-group:last-child { | |
| border-right: none; | |
| } | |
| .tool-btn { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-color); | |
| color: var(--text-secondary); | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 13px; | |
| position: relative; | |
| } | |
| .tool-btn:hover { | |
| background: var(--bg-hover); | |
| color: var(--text-primary); | |
| transform: translateY(-1px); | |
| } | |
| .tool-btn.active { | |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); | |
| color: white; | |
| border-color: var(--accent-blue); | |
| box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); | |
| } | |
| /* Main Content */ | |
| .main-content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| /* DAW Info Bar */ | |
| .daw-info { | |
| background: linear-gradient(90deg, var(--bg-tertiary), var(--bg-secondary)); | |
| padding: 8px 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 30px; | |
| border-bottom: 1px solid var(--border-color); | |
| font-size: 12px; | |
| } | |
| .daw-info-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .daw-info-label { | |
| color: var(--text-secondary); | |
| } | |
| .daw-info-value { | |
| color: var(--accent-blue); | |
| font-weight: 600; | |
| } | |
| /* Editor Container */ | |
| .editor-container { | |
| flex: 1; | |
| position: relative; | |
| background: var(--bg-primary); | |
| overflow: hidden; | |
| } | |
| /* Timeline */ | |
| .timeline { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 45px; | |
| background: linear-gradient(180deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%); | |
| border-bottom: 1px solid var(--border-color); | |
| display: flex; | |
| align-items: center; | |
| padding: 0 15px; | |
| z-index: 10; | |
| } | |
| .time-ruler { | |
| width: 100%; | |
| height: 30px; | |
| position: relative; | |
| } | |
| .time-mark { | |
| position: absolute; | |
| top: 0; | |
| transform: translateX(-50%); | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| font-weight: 500; | |
| } | |
| .time-mark::before { | |
| content: ''; | |
| position: absolute; | |
| top: 20px; | |
| left: 50%; | |
| width: 1px; | |
| height: 5px; | |
| background: var(--text-secondary); | |
| } | |
| /* Piano Roll */ | |
| .piano-roll { | |
| position: absolute; | |
| top: 45px; | |
| left: 0; | |
| width: 60px; | |
| bottom: 0; | |
| background: linear-gradient(90deg, var(--bg-tertiary), var(--bg-secondary)); | |
| border-right: 1px solid var(--border-color); | |
| z-index: 5; | |
| } | |
| .piano-key { | |
| height: 20px; | |
| border-bottom: 1px solid var(--border-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 10px; | |
| color: var(--text-secondary); | |
| position: relative; | |
| } | |
| .piano-key.black { | |
| background: #1a1a1a; | |
| color: var(--text-primary); | |
| height: 15px; | |
| } | |
| .piano-key.white { | |
| background: #252525; | |
| } | |
| /* Canvas */ | |
| #pitchCanvas { | |
| position: absolute; | |
| top: 45px; | |
| left: 60px; | |
| cursor: crosshair; | |
| } | |
| /* Playback Controls */ | |
| .playback-controls { | |
| background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%); | |
| border-top: 1px solid var(--border-color); | |
| padding: 15px 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 25px; | |
| box-shadow: 0 -2px 10px rgba(0,0,0,0.2); | |
| } | |
| .transport { | |
| display: flex; | |
| gap: 5px; | |
| align-items: center; | |
| } | |
| .transport-btn { | |
| width: 42px; | |
| height: 42px; | |
| border-radius: 50%; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-color); | |
| color: var(--text-primary); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.3s; | |
| font-size: 16px; | |
| } | |
| .transport-btn:hover { | |
| background: var(--bg-hover); | |
| transform: scale(1.1); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| } | |
| .transport-btn.play { | |
| width: 50px; | |
| height: 50px; | |
| background: linear-gradient(135deg, var(--success-green), #16a34a); | |
| border-color: var(--success-green); | |
| } | |
| .transport-btn.play:hover { | |
| background: linear-gradient(135deg, #16a34a, #15803d); | |
| } | |
| /* Time Display */ | |
| .time-display { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 18px; | |
| background: var(--bg-tertiary); | |
| border-radius: 10px; | |
| font-size: 14px; | |
| font-variant-numeric: tabular-nums; | |
| border: 1px solid var(--border-color); | |
| } | |
| .time-separator { | |
| color: var(--text-secondary); | |
| } | |
| /* Pitch Display */ | |
| .pitch-display { | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| margin-left: auto; | |
| } | |
| .pitch-info { | |
| padding: 10px 18px; | |
| background: var(--bg-tertiary); | |
| border-radius: 10px; | |
| font-size: 14px; | |
| border: 1px solid var(--border-color); | |
| } | |
| .pitch-label { | |
| color: var(--text-secondary); | |
| margin-right: 8px; | |
| } | |
| .pitch-value { | |
| color: var(--accent-blue); | |
| font-weight: 600; | |
| } | |
| /* Side Panel */ | |
| .side-panel { | |
| position: absolute; | |
| right: 0; | |
| top: 45px; | |
| width: 320px; | |
| height: calc(100% - 45px); | |
| background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%); | |
| border-left: 1px solid var(--border-color); | |
| padding: 25px; | |
| transform: translateX(100%); | |
| transition: transform 0.3s; | |
| z-index: 20; | |
| overflow-y: auto; | |
| box-shadow: -2px 0 10px rgba(0,0,0,0.2); | |
| } | |
| .side-panel.open { | |
| transform: translateX(0); | |
| } | |
| .panel-section { | |
| margin-bottom: 30px; | |
| } | |
| .panel-title { | |
| font-size: 16px; | |
| font-weight: 600; | |
| margin-bottom: 15px; | |
| color: var(--text-primary); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .panel-title::before { | |
| content: ''; | |
| width: 4px; | |
| height: 16px; | |
| background: linear-gradient(180deg, var(--accent-blue), var(--accent-purple)); | |
| border-radius: 2px; | |
| } | |
| .slider-control { | |
| margin-bottom: 20px; | |
| } | |
| .slider-label { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 10px; | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| } | |
| .slider { | |
| width: 100%; | |
| height: 6px; | |
| background: var(--bg-hover); | |
| border-radius: 3px; | |
| outline: none; | |
| -webkit-appearance: none; | |
| } | |
| .slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); | |
| } | |
| .slider::-moz-range-thumb { | |
| width: 18px; | |
| height: 18px; | |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| border: none; | |
| box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); | |
| } | |
| /* Export Options */ | |
| .export-options { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| margin-top: 15px; | |
| } | |
| .export-btn { | |
| background: linear-gradient(135deg, var(--bg-tertiary), var(--bg-hover)); | |
| border: 1px solid var(--border-color); | |
| color: var(--text-primary); | |
| padding: 12px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| font-size: 13px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .export-btn:hover { | |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); | |
| } | |
| /* Loading Overlay */ | |
| .loading-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.9); | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 1000; | |
| } | |
| .loading-overlay.active { | |
| display: flex; | |
| } | |
| .loading-content { | |
| text-align: center; | |
| } | |
| .loading-spinner { | |
| width: 60px; | |
| height: 60px; | |
| border: 3px solid var(--border-color); | |
| border-top-color: var(--accent-blue); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 20px; | |
| } | |
| .loading-text { | |
| color: var(--text-primary); | |
| font-size: 14px; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| /* Tooltip */ | |
| .tooltip { | |
| position: absolute; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-color); | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| font-size: 12px; | |
| pointer-events: none; | |
| z-index: 100; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| } | |
| .tooltip.visible { | |
| opacity: 1; | |
| } | |
| /* DAW Integration Modal */ | |
| .daw-modal { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.9); | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 2000; | |
| } | |
| .daw-modal.active { | |
| display: flex; | |
| } | |
| .modal-content { | |
| background: var(--bg-secondary); | |
| border-radius: 15px; | |
| padding: 30px; | |
| max-width: 600px; | |
| width: 90%; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| box-shadow: 0 10px 40px rgba(0,0,0,0.5); | |
| } | |
| .modal-header { | |
| font-size: 20px; | |
| font-weight: 600; | |
| margin-bottom: 20px; | |
| color: var(--text-primary); | |
| } | |
| .modal-section { | |
| margin-bottom: 25px; | |
| } | |
| .modal-section h3 { | |
| font-size: 16px; | |
| margin-bottom: 10px; | |
| color: var(--accent-blue); | |
| } | |
| .modal-section p { | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| margin-bottom: 10px; | |
| } | |
| .modal-close { | |
| background: var(--accent-red); | |
| color: white; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| margin-top: 20px; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .toolbar { | |
| padding: 8px 12px; | |
| } | |
| .tool-group { | |
| padding: 0 8px; | |
| } | |
| .tool-btn { | |
| padding: 6px 8px; | |
| font-size: 12px; | |
| } | |
| .side-panel { | |
| width: 100%; | |
| } | |
| .header-controls { | |
| gap: 8px; | |
| } | |
| .daw-info { | |
| flex-wrap: wrap; | |
| gap: 15px; | |
| } | |
| } | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 10px; | |
| height: 10px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg-primary); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--bg-hover); | |
| border-radius: 5px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--border-color); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="logo"> | |
| <div class="logo-icon">♪</div> | |
| <span>PitchLab Pro</span> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" | |
| style="color: var(--text-secondary); font-size: 12px; margin-left: 8px;">Built with anycoder</a> | |
| </div> | |
| <div class="header-controls"> | |
| <button class="header-btn" onclick="showDAWIntegration()"> | |
| 🎹 DAW Integration | |
| </button> | |
| <button class="header-btn" onclick="toggleSidePanel()"> | |
| ⚙️ Settings | |
| </button> | |
| <button class="header-btn" onclick="exportProject()"> | |
| 💾 Export Project | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Toolbar --> | |
| <div class="toolbar"> | |
| <div class="tool-group"> | |
| <button class="tool-btn active" data-tool="select" onclick="selectTool('select')"> | |
| ↖️ Select | |
| </button> | |
| <button class="tool-btn" data-tool="pitch" onclick="selectTool('pitch')"> | |
| 🎵 Pitch | |
| </button> | |
| <button class="tool-btn" data-tool="time" onclick="selectTool('time')"> | |
| ⏱️ Time | |
| </button> | |
| <button class="tool-btn" data-tool="slice" onclick="selectTool('slice')"> | |
| ✂️ Slice | |
| </button> | |
| </div> | |
| <div class="tool-group"> | |
| <button class="tool-btn" onclick="resetView()"> | |
| 🔄 Reset View | |
| </button> | |
| <button class="tool-btn" onclick="zoomIn()"> | |
| 🔍+ Zoom In | |
| </button> | |
| <button class="tool-btn" onclick="zoomOut()"> | |
| 🔍- Zoom Out | |
| </button> | |
| </div> | |
| <div class="tool-group"> | |
| <div class="file-input-wrapper"> | |
| <button class="tool-btn" onclick="document.getElementById('audioFile').click()"> | |
| 📁 Load Audio | |
| </button> | |
| <input type="file" id="audioFile" class="file-input" accept="audio/*" onchange="loadAudioFile(event)"> | |
| </div> | |
| <button class="tool-btn" onclick="document.getElementById('midiFile').click()"> | |
| 🎹 Import MIDI | |
| </button> | |
| <input type="file" id="midiFile" class="file-input" accept=".mid,.midi" onchange="loadMIDIFile(event)"> | |
| <button class="tool-btn" onclick="clearAll()"> | |
| 🗑️ Clear | |
| </button> | |
| </div> | |
| </div> | |
| <!-- DAW Info Bar --> | |
| <div class="daw-info"> | |
| <div class="daw-info-item"> | |
| <span class="daw-info-label">Tempo:</span> | |
| <span class="daw-info-value" id="bpmValue">120 BPM</span> | |
| </div> | |
| <div class="daw-info-item"> | |
| <span class="daw-info-label">Key:</span> | |
| <span class="daw-info-value" id="keyValue">C Major</span> | |
| </div> | |
| <div class="daw-info-item"> | |
| <span class="daw-info-label">Time Sig:</span> | |
| <span class="daw-info-value" id="timeSigValue">4/4</span> | |
| </div> | |
| <div class="daw-info-item"> | |
| <span class="daw-info-label">Sample Rate:</span> | |
| <span class="daw-info-value" id="sampleRateValue">44.1 kHz</span> | |
| </div> | |
| <div class="daw-info-item"> | |
| <span class="daw-info-label">Format:</span> | |
| <span class="daw-info-value" id="formatValue">Ready for DAW</span> | |
| </div> | |
| </div> | |
| <!-- Main Editor --> | |
| <div class="main-content"> | |
| <div class="editor-container"> | |
| <!-- Timeline --> | |
| <div class="timeline"> | |
| <div class="time-ruler" id="timeRuler"></div> | |
| </div> | |
| <!-- Piano Roll --> | |
| <div class="piano-roll" id="pianoRoll"></div> | |
| <!-- Pitch Canvas --> | |
| <canvas id="pitchCanvas"></canvas> | |
| <!-- Side Panel --> | |
| <div class="side-panel" id="sidePanel"> | |
| <div class="panel-section"> | |
| <h3 class="panel-title">DAW Settings</h3> | |
| <div class="slider-control"> | |
| <div class="slider-label"> | |
| <span>Tempo (BPM)</span> | |
| <span id="bpmSlider">120</span> | |
| </div> | |
| <input type="range" class="slider" min="60" max="200" value="120" oninput="updateBPM(this.value)"> | |
| </div> | |
| <div class="slider-control"> | |
| <div class="slider-label"> | |
| <span>Quantize Strength</span> | |
| <span id="quantizeValue">50%</span> | |
| </div> | |
| <input type="range" class="slider" min="0" max="100" value="50" oninput="updateSlider('quantize', this.value)"> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <h3 class="panel-title">Pitch Correction</h3> | |
| <div class="slider-control"> | |
| <div class="slider-label"> | |
| <span>Snap Strength</span> | |
| <span id="snapValue">50%</span> | |
| </div> | |
| <input type="range" class="slider" min="0" max="100" value="50" oninput="updateSlider('snap', this.value)"> | |
| </div> | |
| <div class="slider-control"> | |
| <div class="slider-label"> | |
| <span>Pitch Range</span> | |
| <span id="rangeValue">±12</span> | |
| </div> | |
| <input type="range" class="slider" min="1" max="24" value="12" oninput="updateSlider('range', this.value)"> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <h3 class="panel-title">Export Options</h3> | |
| <div class="export-options"> | |
| <button class="export-btn" onclick="exportMIDI()"> | |
| 🎹 Export as MIDI | |
| </button> | |
| <button class="export-btn" onclick="exportAudioWithPitch()"> | |
| 🎵 Export Corrected Audio | |
| </button> | |
| <button class="export-btn" onclick="exportMelodyneXML()"> | |
| 📄 Export Melodyne XML | |
| </button> | |
| <button class="export-btn" onclick="exportAbletonClip()"> | |
| 🎛️ Export Ableton Clip | |
| </button> | |
| <button class="export-btn" onclick="exportLogicPX()"> | |
| 🎼 Export Logic Pro PX | |
| </button> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <h3 class="panel-title">Display</h3> | |
| <div class="slider-control"> | |
| <div class="slider-label"> | |
| <span>Vertical Zoom</span> | |
| <span id="vZoomValue">100%</span> | |
| </div> | |
| <input type="range" class="slider" min="50" max="200" value="100" oninput="updateSlider('vZoom', this.value)"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Playback Controls --> | |
| <div class="playback-controls"> | |
| <div class="transport"> | |
| <button class="transport-btn" onclick="skipBackward()">⏮️</button> | |
| <button class="transport-btn" onclick="stepBackward()">⏪</button> | |
| <button class="transport-btn play" onclick="togglePlayback()">▶️</button> | |
| <button class="transport-btn" onclick="stepForward()">⏩</button> | |
| <button class="transport-btn" onclick="skipForward()">⏭️</button> | |
| <button class="transport-btn" onclick="loopToggle()">🔁</button> | |
| </div> | |
| <div class="time-display"> | |
| <span id="currentTime">00:00.000</span> | |
| <span class="time-separator">/</span> | |
| <span id="totalTime">00:00.000</span> | |
| </div> | |
| <div class="pitch-display"> | |
| <div class="pitch-info"> | |
| <span class="pitch-label">Current Note:</span> | |
| <span class="pitch-value" id="currentNote">--</span> | |
| </div> | |
| <div class="pitch-info"> | |
| <span class="pitch-label">Frequency:</span> | |
| <span class="pitch-value" id="currentFreq">-- Hz</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Loading Overlay --> | |
| <div class="loading-overlay" id="loadingOverlay"> | |
| <div class="loading-content"> | |
| <div class="loading-spinner"></div> | |
| <div class="loading-text">Processing audio...</div> | |
| </div> | |
| </div> | |
| <!-- DAW Integration Modal --> | |
| <div class="daw-modal" id="dawModal"> | |
| <div class="modal-content"> | |
| <h2 class="modal-header">🎹 DAW Integration Guide</h2> | |
| <div class="modal-section"> | |
| <h3>How to use PitchLab Pro with your DAW:</h3> | |
| <p><strong>1. Export Options:</strong></p> | |
| <p>• <strong>MIDI Export:</strong> Export your edited melody as a MIDI file that can be dragged into any DAW</p> | |
| <p>• <strong>Audio Export:</strong> Export the pitch-corrected audio with applied changes</p> | |
| <p>• <strong>DAW-Specific Formats:</strong> Export in formats compatible with Ableton Live, Logic Pro, and more</p> | |
| </div> | |
| <div class="modal-section"> | |
| <h3>Workflow Integration:</h3> | |
| <p><strong>Ableton Live:</strong></p> | |
| <p>• Export as MIDI and drag onto a MIDI track</p> | |
| <p>• Use "Export Ableton Clip" for seamless integration</p> | |
| <p>• Tempo and time signature are automatically matched</p> | |
| <p><strong>Logic Pro:</strong></p> | |
| <p>• Export as Logic PX format for direct import</p> | |
| <p>• Maintains all pitch correction data</p> | |
| <p>• Compatible with Logic's Flex Pitch</p> | |
| <p><strong>FL Studio:</strong></p> | |
| <p>• Export as MIDI file</p> | |
| <p>• Drag into Piano Roll</p> | |
| <p>• Use with Newtone or Pitcher for further processing</p> | |
| </div> | |
| <div class="modal-section"> | |
| <h3>Real-time Integration:</h3> | |
| <p>• Use your DAW's ReWire or virtual audio cable to route audio</p> | |
| <p>• Set BPM to match your project tempo</p> | |
| <p>• Export stems for each edited section</p> | |
| </div> | |
| <button class="modal-close" onclick="closeDAWModal()">Got it!</button> | |
| </div> | |
| </div> | |
| <script> | |
| // Global variables | |
| let canvas, ctx; | |
| let audioContext; | |
| let audioBuffer; | |
| let source; | |
| let isPlaying = false; | |
| let startTime = 0; | |
| let pauseTime = 0; | |
| let currentTool = 'select'; | |
| let zoomLevel = 1; | |
| let verticalZoom = 1; | |
| let notes = []; | |
| let selectedNote = null; | |
| let isDragging = false; | |
| let dragStartX = 0; | |
| let dragStartY = 0; | |
| let viewOffset = 0; | |
| let playbackPosition = 0; | |
| let projectBPM = 120; | |
| let projectKey = 'C'; | |
| let timeSignature = '4/4'; | |
| let isLooping = false; | |
| // Note frequencies for C2 to B6 | |
| const noteFrequencies = { | |
| 'C2': 65.41, 'C#2': 69.30, 'D2': 73.42, 'D#2': 77.78, 'E2': 82.41, 'F2': 87.31, | |
| 'F#2': 92.50, 'G2': 98.00, 'G#2': 103.83, 'A2': 110.00, 'A#2': 116.54, 'B2': 123.47, | |
| 'C3': 130.81, 'C#3': 138.59, 'D3': 146.83, 'D#3': 155.56, 'E3': 164.81, 'F3': 174.61, | |
| 'F#3': 185.00, 'G3': 196.00, 'G#3': 207.65, 'A3': 220.00, 'A#3': 233.08, 'B3': 246.94, | |
| 'C4': 261.63, 'C#4': 277.18, 'D4': 293.66, 'D#4': 311.13, 'E4': 329.63, 'F4': 349.23, | |
| 'F#4': 369.99, 'G4': 392.00, 'G#4': 415.30, 'A4': 440.00, 'A#4': 466.16, 'B4': 493.88, | |
| 'C5': 523.25, 'C#5': 554.37, 'D5': 587.33, 'D#5': 622.25, 'E5': 659.25, 'F5': 698.46, | |
| 'F#5': 739.99, 'G5': 783.99, 'G#5': 830.61, 'A5': 880.00, 'A#5': 932.33, 'B5': 987.77, | |
| 'C6': 1046.50, 'C#6': 1108.73, 'D6': 1174.66, 'D#6': 1244.51, 'E6': 1318.51, 'F6': 1396.91 | |
| }; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initCanvas(); | |
| initAudioContext(); | |
| initPianoRoll(); | |
| generateDemoNotes(); | |
| drawInterface(); | |
| updateTimeRuler(); | |
| startAnimationLoop(); | |
| }); | |
| function initCanvas() { | |
| canvas = document.getElementById('pitchCanvas'); | |
| ctx = canvas.getContext('2d'); | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| canvas.addEventListener('mousedown', handleMouseDown); | |
| canvas.addEventListener('mousemove', handleMouseMove); | |
| canvas.addEventListener('mouseup', handleMouseUp); | |
| canvas.addEventListener('wheel', handleWheel); | |
| } | |
| function resizeCanvas() { | |
| const container = document.querySelector('.editor-container'); | |
| canvas.width = container.clientWidth - 60; | |
| canvas.height = container.clientHeight - 45; | |
| drawInterface(); | |
| } | |
| function initPianoRoll() { | |
| const pianoRoll = document.getElementById('pianoRoll'); | |
| pianoRoll.innerHTML = ''; | |
| const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; | |
| for (let octave = 6; octave >= 2; octave--) { | |
| for (let i = noteNames.length - 1; i >= 0; i--) { | |
| const noteName = noteNames[i] + octave; | |
| const key = document.createElement('div'); | |
| key.className = 'piano-key'; | |
| if (noteNames[i].includes('#')) { | |
| key.classList.add('black'); | |
| } else { | |
| key.classList.add('white'); | |
| } | |
| key.textContent = noteName; | |
| pianoRoll.appendChild(key); | |
| } | |
| } | |
| } | |
| function initAudioContext() { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| function generateDemoNotes() { | |
| const demoNotes = [ | |
| { note: 'C4', start: 0, duration: 0.5, pitch: 261.63, velocity: 80 }, | |
| { note: 'E4', start: 0.5, duration: 0.5, pitch: 329.63, velocity: 75 }, | |
| { note: 'G4', start: 1.0, duration: 0.5, pitch: 392.00, velocity: 70 }, | |
| { note: 'C5', start: 1.5, duration: 0.5, pitch: 523.25, velocity: 85 }, | |
| { note: 'G4', start: 2.0, duration: 0.5, pitch: 392.00, velocity: 70 }, | |
| { note: 'E4', start: 2.5, duration: 0.5, pitch: 329 |