Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Waveform - Professional Audio Editor</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
/* Custom CSS for audio waveform */ | |
.waveform-container { | |
height: 120px; | |
background: linear-gradient(90deg, rgba(30,58,138,0.2) 0%, rgba(79,70,229,0.2) 100%); | |
border-radius: 8px; | |
position: relative; | |
overflow: hidden; | |
} | |
.waveform { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
} | |
.waveform-bar { | |
width: 3px; | |
background-color: #4f46e5; | |
border-radius: 3px; | |
transition: height 0.3s ease; | |
} | |
.playhead { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 2px; | |
height: 100%; | |
background-color: #ffffff; | |
z-index: 10; | |
box-shadow: 0 0 5px rgba(255,255,255,0.8); | |
} | |
.effect-card:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |
} | |
/* Selection area for trimming */ | |
.selection-area { | |
position: absolute; | |
top: 0; | |
height: 100%; | |
background-color: rgba(79, 70, 229, 0.3); | |
border-left: 2px solid #4f46e5; | |
border-right: 2px solid #4f46e5; | |
z-index: 5; | |
cursor: move; | |
} | |
.selection-handle { | |
position: absolute; | |
top: 0; | |
width: 10px; | |
height: 100%; | |
background-color: rgba(255, 255, 255, 0.8); | |
cursor: ew-resize; | |
z-index: 6; | |
} | |
.selection-handle-left { | |
left: -5px; | |
} | |
.selection-handle-right { | |
right: -5px; | |
} | |
/* Custom scrollbar */ | |
::-webkit-scrollbar { | |
width: 8px; | |
height: 8px; | |
} | |
::-webkit-scrollbar-track { | |
background: #f1f1f1; | |
border-radius: 10px; | |
} | |
::-webkit-scrollbar-thumb { | |
background: #888; | |
border-radius: 10px; | |
} | |
::-webkit-scrollbar-thumb:hover { | |
background: #555; | |
} | |
/* Animation for recording */ | |
@keyframes pulse { | |
0% { | |
transform: scale(1); | |
opacity: 1; | |
} | |
50% { | |
transform: scale(1.1); | |
opacity: 0.7; | |
} | |
100% { | |
transform: scale(1); | |
opacity: 1; | |
} | |
} | |
.recording-animation { | |
animation: pulse 1.5s infinite; | |
} | |
/* Tooltip styles */ | |
.tooltip { | |
position: relative; | |
display: inline-block; | |
} | |
.tooltip .tooltiptext { | |
visibility: hidden; | |
width: 120px; | |
background-color: #555; | |
color: #fff; | |
text-align: center; | |
border-radius: 6px; | |
padding: 5px; | |
position: absolute; | |
z-index: 1; | |
bottom: 125%; | |
left: 50%; | |
margin-left: -60px; | |
opacity: 0; | |
transition: opacity 0.3s; | |
} | |
.tooltip:hover .tooltiptext { | |
visibility: visible; | |
opacity: 1; | |
} | |
/* Effect panel styles */ | |
.effect-panel { | |
transition: all 0.3s ease; | |
max-height: 0; | |
overflow: hidden; | |
} | |
.effect-panel.open { | |
max-height: 500px; | |
padding: 1rem; | |
border: 1px solid #e5e7eb; | |
border-radius: 0.5rem; | |
margin-top: 0.5rem; | |
} | |
/* Track item styles */ | |
.track-item { | |
transition: all 0.2s ease; | |
} | |
.track-item.active { | |
border-color: #4f46e5; | |
background-color: #f5f3ff; | |
} | |
/* Timeline ruler */ | |
.timeline-ruler { | |
height: 30px; | |
background-color: #f3f4f6; | |
border-bottom: 1px solid #e5e7eb; | |
position: relative; | |
overflow: hidden; | |
} | |
.timeline-marker { | |
position: absolute; | |
bottom: 0; | |
height: 100%; | |
border-right: 1px solid #d1d5db; | |
} | |
.timeline-marker-label { | |
position: absolute; | |
bottom: 5px; | |
font-size: 0.75rem; | |
color: #6b7280; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50 text-gray-800 font-sans"> | |
<!-- Navigation --> | |
<nav class="bg-indigo-900 text-white shadow-lg"> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
<div class="flex justify-between h-16 items-center"> | |
<div class="flex items-center"> | |
<i class="fas fa-wave-square text-2xl mr-2 text-indigo-300"></i> | |
<span class="text-xl font-bold">Waveform Pro</span> | |
</div> | |
<div class="hidden md:flex items-center space-x-8"> | |
<a href="#" class="hover:text-indigo-200 transition">File</a> | |
<a href="#" class="hover:text-indigo-200 transition">Edit</a> | |
<a href="#" class="hover:text-indigo-200 transition">View</a> | |
<a href="#" class="hover:text-indigo-200 transition">Help</a> | |
</div> | |
<div class="flex items-center space-x-4"> | |
<button class="px-4 py-2 rounded-md bg-indigo-700 hover:bg-indigo-600 transition">Save</button> | |
<button class="px-4 py-2 rounded-md bg-white text-indigo-900 hover:bg-gray-100 transition">Export</button> | |
</div> | |
</div> | |
</div> | |
</nav> | |
<!-- Main Editor Section --> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> | |
<div class="flex flex-col lg:flex-row gap-4"> | |
<!-- Left Sidebar - Tracks --> | |
<div class="lg:w-1/4 bg-white rounded-xl shadow-md p-4"> | |
<div class="flex justify-between items-center mb-4"> | |
<h2 class="text-lg font-bold">Tracks</h2> | |
<button id="addTrackBtn" class="text-indigo-600 hover:text-indigo-800"> | |
<i class="fas fa-plus"></i> Add Track | |
</button> | |
</div> | |
<div id="tracksContainer" class="space-y-3"> | |
<!-- Tracks will be added here dynamically --> | |
</div> | |
<div class="mt-4 pt-4 border-t border-gray-200"> | |
<h3 class="font-medium mb-2">Master Track</h3> | |
<div class="flex items-center space-x-3"> | |
<div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600"> | |
<i class="fas fa-sliders-h"></i> | |
</div> | |
<div class="flex-1"> | |
<input type="range" min="0" max="100" value="80" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> | |
</div> | |
<span class="text-sm text-gray-500">-2.4dB</span> | |
</div> | |
</div> | |
</div> | |
<!-- Main Editor Area --> | |
<div class="lg:w-2/4 bg-white rounded-xl shadow-md p-4"> | |
<!-- Timeline Ruler --> | |
<div class="timeline-ruler mb-2" id="timelineRuler"> | |
<!-- Timeline markers will be added here --> | |
</div> | |
<!-- Waveform Display --> | |
<div class="waveform-container mb-4 relative" id="mainWaveformContainer"> | |
<div class="waveform" id="mainWaveform"></div> | |
<div class="playhead" id="mainPlayhead"></div> | |
<div class="selection-area hidden" id="selectionArea"> | |
<div class="selection-handle selection-handle-left"></div> | |
<div class="selection-handle selection-handle-right"></div> | |
</div> | |
</div> | |
<!-- Audio Clips --> | |
<div id="audioClipsContainer" class="bg-gray-50 rounded-lg p-3 mb-4 min-h-16"> | |
<!-- Audio clips will be added here --> | |
</div> | |
<!-- Transport Controls --> | |
<div class="flex justify-between items-center"> | |
<div class="flex space-x-3"> | |
<button id="playBtn" class="w-12 h-12 rounded-full bg-indigo-600 text-white hover:bg-indigo-700 flex items-center justify-center tooltip"> | |
<i class="fas fa-play"></i> | |
<span class="tooltiptext">Play (Space)</span> | |
</button> | |
<button id="pauseBtn" class="w-12 h-12 rounded-full bg-gray-200 hover:bg-gray-300 flex items-center justify-center tooltip"> | |
<i class="fas fa-pause"></i> | |
<span class="tooltiptext">Pause (Space)</span> | |
</button> | |
<button id="stopBtn" class="w-12 h-12 rounded-full bg-gray-200 hover:bg-gray-300 flex items-center justify-center tooltip"> | |
<i class="fas fa-stop"></i> | |
<span class="tooltiptext">Stop</span> | |
</button> | |
<button id="loopBtn" class="w-12 h-12 rounded-full bg-gray-200 hover:bg-gray-300 flex items-center justify-center tooltip"> | |
<i class="fas fa-redo"></i> | |
<span class="tooltiptext">Loop</span> | |
</button> | |
</div> | |
<div class="flex items-center space-x-4"> | |
<div class="flex items-center space-x-2"> | |
<span class="text-sm text-gray-600">Speed:</span> | |
<input type="range" id="speedControl" min="50" max="200" value="100" class="w-24"> | |
<span id="speedValue" class="text-sm font-medium w-10">1.0x</span> | |
</div> | |
<div id="timeDisplay" class="text-gray-600">00:00:00 / 00:03:45</div> | |
</div> | |
</div> | |
<!-- Zoom Controls --> | |
<div class="mt-4 flex justify-between items-center"> | |
<div class="flex space-x-2"> | |
<button id="zoomInBtn" class="px-3 py-1 rounded-md bg-gray-100 hover:bg-gray-200 text-sm"> | |
<i class="fas fa-search-plus"></i> Zoom In | |
</button> | |
<button id="zoomOutBtn" class="px-3 py-1 rounded-md bg-gray-100 hover:bg-gray-200 text-sm"> | |
<i class="fas fa-search-minus"></i> Zoom Out | |
</button> | |
<button id="zoomFitBtn" class="px-3 py-1 rounded-md bg-gray-100 hover:bg-gray-200 text-sm"> | |
<i class="fas fa-expand"></i> Fit to View | |
</button> | |
</div> | |
<div class="text-sm text-gray-500">Zoom: <span id="zoomLevel">100%</span></div> | |
</div> | |
</div> | |
<!-- Right Sidebar - Tools & Effects --> | |
<div class="lg:w-1/4 bg-white rounded-xl shadow-md p-4"> | |
<div class="flex justify-between items-center mb-4"> | |
<h2 class="text-lg font-bold">Tools</h2> | |
</div> | |
<div class="grid grid-cols-4 gap-2 mb-6"> | |
<button id="selectTool" class="p-2 rounded-md bg-indigo-100 text-indigo-700 hover:bg-indigo-200 flex flex-col items-center tooltip"> | |
<i class="fas fa-mouse-pointer mb-1"></i> | |
<span class="text-xs">Select</span> | |
<span class="tooltiptext">Selection Tool (V)</span> | |
</button> | |
<button id="cutTool" class="p-2 rounded-md bg-gray-100 hover:bg-gray-200 flex flex-col items-center tooltip"> | |
<i class="fas fa-cut mb-1"></i> | |
<span class="text-xs">Cut</span> | |
<span class="tooltiptext">Cut Tool (C)</span> | |
</button> | |
<button id="trimTool" class="p-2 rounded-md bg-gray-100 hover:bg-gray-200 flex flex-col items-center tooltip"> | |
<i class="fas fa-arrows-alt-h mb-1"></i> | |
<span class="text-xs">Trim</span> | |
<span class="tooltiptext">Trim Tool (T)</span> | |
</button> | |
<button id="fadeTool" class="p-2 rounded-md bg-gray-100 hover:bg-gray-200 flex flex-col items-center tooltip"> | |
<i class="fas fa-wave-square mb-1"></i> | |
<span class="text-xs">Fade</span> | |
<span class="tooltiptext">Fade Tool (F)</span> | |
</button> | |
</div> | |
<div class="mb-6"> | |
<div class="flex justify-between items-center mb-3"> | |
<h3 class="font-medium">Selected Region</h3> | |
<button id="clearSelectionBtn" class="text-xs text-indigo-600 hover:text-indigo-800">Clear</button> | |
</div> | |
<div id="selectionInfo" class="text-sm text-gray-500 p-3 bg-gray-50 rounded-md"> | |
No selection made | |
</div> | |
<div class="mt-3 grid grid-cols-2 gap-2"> | |
<button id="trimSelectionBtn" class="px-3 py-1 text-sm rounded-md bg-gray-100 hover:bg-gray-200">Trim</button> | |
<button id="deleteSelectionBtn" class="px-3 py-1 text-sm rounded-md bg-gray-100 hover:bg-gray-200">Delete</button> | |
<button id="fadeInBtn" class="px-3 py-1 text-sm rounded-md bg-gray-100 hover:bg-gray-200">Fade In</button> | |
<button id="fadeOutBtn" class="px-3 py-1 text-sm rounded-md bg-gray-100 hover:bg-gray-200">Fade Out</button> | |
</div> | |
</div> | |
<div> | |
<div class="flex justify-between items-center mb-3"> | |
<h3 class="font-medium">Effects</h3> | |
<button id="addEffectBtn" class="text-xs text-indigo-600 hover:text-indigo-800">Add Effect</button> | |
</div> | |
<div id="effectsList" class="space-y-3"> | |
<!-- Effects will be added here --> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Effect Panel Template (hidden) --> | |
<div id="effectPanelTemplate" class="effect-panel hidden"> | |
<div class="flex justify-between items-center mb-2"> | |
<h4 class="font-medium">Effect Settings</h4> | |
<button class="text-gray-500 hover:text-gray-700 close-effect-panel"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="space-y-3"> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Parameter 1</label> | |
<input type="range" min="0" max="100" value="50" class="w-full"> | |
</div> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Parameter 2</label> | |
<input type="range" min="0" max="100" value="50" class="w-full"> | |
</div> | |
<button class="w-full py-1 text-sm rounded-md bg-red-100 text-red-700 hover:bg-red-200 remove-effect"> | |
Remove Effect | |
</button> | |
</div> | |
</div> | |
<!-- Track Item Template (hidden) --> | |
<div id="trackItemTemplate" class="track-item border border-gray-200 rounded-lg p-3 hover:border-indigo-300 transition hidden"> | |
<div class="flex justify-between items-center mb-2"> | |
<span class="font-medium track-name">Track 1</span> | |
<div class="flex space-x-2"> | |
<button class="text-gray-500 hover:text-indigo-600 toggle-mute"> | |
<i class="fas fa-volume-up"></i> | |
</button> | |
<button class="text-gray-500 hover:text-indigo-600 toggle-solo"> | |
<i class="fas fa-headphones"></i> | |
</button> | |
<button class="text-gray-500 hover:text-indigo-600 toggle-visibility"> | |
<i class="fas fa-eye"></i> | |
</button> | |
<button class="text-gray-500 hover:text-indigo-600 delete-track"> | |
<i class="fas fa-trash"></i> | |
</button> | |
</div> | |
</div> | |
<div class="h-2 bg-gray-100 rounded-full overflow-hidden"> | |
<div class="h-full bg-indigo-500 volume-level w-3/4"></div> | |
</div> | |
<input type="range" min="-30" max="6" value="0" step="0.1" class="w-full mt-2 volume-slider"> | |
<div class="text-xs text-gray-500 text-right mt-1 volume-db">0.0 dB</div> | |
</div> | |
<!-- Audio Clip Template (hidden) --> | |
<div id="audioClipTemplate" class="flex-shrink-0 h-16 rounded-md border border-gray-300 bg-white hover:border-indigo-400 transition relative hidden"> | |
<div class="absolute inset-0 flex items-center justify-start pl-2 overflow-hidden"> | |
<span class="text-sm truncate audio-clip-name">Audio Clip</span> | |
</div> | |
<div class="absolute top-0 right-0 p-1"> | |
<button class="text-gray-500 hover:text-indigo-600 text-xs delete-clip"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="absolute bottom-0 left-0 right-0 h-1 bg-gray-100"> | |
<div class="h-full bg-indigo-400 audio-clip-waveform"></div> | |
</div> | |
</div> | |
<script> | |
// Global variables | |
let currentTool = 'select'; | |
let isPlaying = false; | |
let currentTime = 0; | |
let totalDuration = 225; // 3:45 in seconds | |
let zoomLevel = 100; | |
let selectionStart = 0; | |
let selectionEnd = 0; | |
let hasSelection = false; | |
let trackCount = 0; | |
let clipCount = 0; | |
// DOM elements | |
const mainWaveformContainer = document.getElementById('mainWaveformContainer'); | |
const selectionArea = document.getElementById('selectionArea'); | |
const selectionInfo = document.getElementById('selectionInfo'); | |
const timeDisplay = document.getElementById('timeDisplay'); | |
const zoomLevelDisplay = document.getElementById('zoomLevel'); | |
const speedControl = document.getElementById('speedControl'); | |
const speedValue = document.getElementById('speedValue'); | |
// Initialize the application | |
document.addEventListener('DOMContentLoaded', function() { | |
// Generate demo waveform | |
generateWaveform('mainWaveform', 500); | |
// Create timeline markers | |
createTimelineMarkers(); | |
// Add event listeners | |
setupEventListeners(); | |
// Add a couple of demo tracks | |
addTrack('Vocals'); | |
addTrack('Guitar'); | |
addTrack('Drums'); | |
// Add some demo audio clips | |
addAudioClip('Vocals', 'Verse 1', 0, 30); | |
addAudioClip('Vocals', 'Chorus', 30, 45); | |
addAudioClip('Guitar', 'Rhythm', 0, 45); | |
addAudioClip('Drums', 'Drum Loop', 0, 45); | |
// Start the playhead animation | |
animatePlayhead(); | |
// Update time display | |
updateTimeDisplay(); | |
}); | |
// Generate waveform visualization | |
function generateWaveform(elementId, barCount) { | |
const container = document.getElementById(elementId); | |
container.innerHTML = ''; | |
for (let i = 0; i < barCount; i++) { | |
const bar = document.createElement('div'); | |
bar.className = 'waveform-bar'; | |
// Create a more interesting waveform pattern | |
const pos = i / barCount; | |
let height; | |
if (pos < 0.2) { | |
// Intro section - quieter | |
height = 20 + Math.sin(pos * 20) * 15 + Math.random() * 10; | |
} else if (pos > 0.8) { | |
// Outro section - quieter | |
height = 20 + Math.sin(pos * 25) * 10 + Math.random() * 8; | |
} else { | |
// Main section - louder with more variation | |
height = 40 + Math.sin(pos * 50) * 30 + Math.random() * 15; | |
// Add some "peaks" | |
if (Math.random() > 0.95) { | |
height += 30; | |
} | |
} | |
// Ensure height is within bounds | |
height = Math.max(5, Math.min(100, height)); | |
bar.style.height = `${height}%`; | |
container.appendChild(bar); | |
} | |
} | |
// Create timeline markers | |
function createTimelineMarkers() { | |
const ruler = document.getElementById('timelineRuler'); | |
ruler.innerHTML = ''; | |
const totalWidth = ruler.offsetWidth; | |
const totalSeconds = totalDuration; | |
const pixelsPerSecond = totalWidth / (totalSeconds * (zoomLevel / 100)); | |
// Add markers every 5 seconds | |
for (let seconds = 0; seconds <= totalSeconds; seconds += 5) { | |
const marker = document.createElement('div'); | |
marker.className = 'timeline-marker'; | |
marker.style.left = `${seconds * pixelsPerSecond}px`; | |
// Add label for every 15 seconds | |
if (seconds % 15 === 0) { | |
const label = document.createElement('div'); | |
label.className = 'timeline-marker-label'; | |
label.style.left = `${seconds * pixelsPerSecond + 2}px`; | |
label.textContent = formatTime(seconds); | |
ruler.appendChild(label); | |
} | |
ruler.appendChild(marker); | |
} | |
} | |
// Format time as MM:SS | |
function formatTime(seconds) { | |
const mins = Math.floor(seconds / 60); | |
const secs = Math.floor(seconds % 60); | |
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
} | |
// Format time as HH:MM:SS | |
function formatTimeLong(seconds) { | |
const hours = Math.floor(seconds / 3600); | |
const mins = Math.floor((seconds % 3600) / 60); | |
const secs = Math.floor(seconds % 60); | |
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
} | |
// Update the time display | |
function updateTimeDisplay() { | |
timeDisplay.textContent = `${formatTimeLong(currentTime)} / ${formatTimeLong(totalDuration)}`; | |
} | |
// Animate the playhead | |
function animatePlayhead() { | |
const playhead = document.getElementById('mainPlayhead'); | |
const container = document.getElementById('mainWaveformContainer'); | |
function updatePlayheadPosition() { | |
if (isPlaying) { | |
currentTime += 0.1 * (parseInt(speedControl.value) / 100); | |
if (currentTime > totalDuration) { | |
currentTime = 0; | |
if (!document.getElementById('loopBtn').classList.contains('bg-indigo-100')) { | |
isPlaying = false; | |
document.getElementById('playBtn').classList.remove('bg-indigo-600'); | |
document.getElementById('playBtn').classList.add('bg-gray-200'); | |
} | |
} | |
updateTimeDisplay(); | |
} | |
const percentage = (currentTime / totalDuration) * 100; | |
playhead.style.left = `${percentage}%`; | |
requestAnimationFrame(updatePlayheadPosition); | |
} | |
updatePlayheadPosition(); | |
} | |
// Setup event listeners | |
function setupEventListeners() { | |
// Tool buttons | |
document.getElementById('selectTool').addEventListener('click', () => setActiveTool('select')); | |
document.getElementById('cutTool').addEventListener('click', () => setActiveTool('cut')); | |
document.getElementById('trimTool').addEventListener('click', () => setActiveTool('trim')); | |
document.getElementById('fadeTool').addEventListener('click', () => setActiveTool('fade')); | |
// Transport controls | |
document.getElementById('playBtn').addEventListener('click', togglePlay); | |
document.getElementById('pauseBtn').addEventListener('click', togglePlay); | |
document.getElementById('stopBtn').addEventListener('click', stopPlayback); | |
document.getElementById('loopBtn').addEventListener('click', toggleLoop); | |
// Zoom controls | |
document.getElementById('zoomInBtn').addEventListener('click', () => adjustZoom(10)); | |
document.getElementById('zoomOutBtn').addEventListener('click', () => adjustZoom(-10)); | |
document.getElementById('zoomFitBtn').addEventListener('click', fitToView); | |
// Speed control | |
speedControl.addEventListener('input', updateSpeed); | |
// Selection controls | |
document.getElementById('clearSelectionBtn').addEventListener('click', clearSelection); | |
document.getElementById('trimSelectionBtn').addEventListener('click', trimSelection); | |
document.getElementById('deleteSelectionBtn').addEventListener('click', deleteSelection); | |
document.getElementById('fadeInBtn').addEventListener('click', () => applyFade('in')); | |
document.getElementById('fadeOutBtn').addEventListener('click', () => applyFade('out')); | |
// Track controls | |
document.getElementById('addTrackBtn').addEventListener('click', () => addTrack(`Track ${trackCount + 1}`)); | |
// Effect controls | |
document.getElementById('addEffectBtn').addEventListener('click', addEffect); | |
// Waveform interaction | |
mainWaveformContainer.addEventListener('mousedown', handleWaveformMouseDown); | |
mainWaveformContainer.addEventListener('mousemove', handleWaveformMouseMove); | |
mainWaveformContainer.addEventListener('mouseup', handleWaveformMouseUp); | |
mainWaveformContainer.addEventListener('mouseleave', handleWaveformMouseUp); | |
// Keyboard shortcuts | |
document.addEventListener('keydown', handleKeyboardShortcuts); | |
} | |
// Set active tool | |
function setActiveTool(tool) { | |
currentTool = tool; | |
// Update UI | |
document.getElementById('selectTool').classList.remove('bg-indigo-100', 'text-indigo-700'); | |
document.getElementById('cutTool').classList.remove('bg-indigo-100', 'text-indigo-700'); | |
document.getElementById('trimTool').classList.remove('bg-indigo-100', 'text-indigo-700'); | |
document.getElementById('fadeTool').classList.remove('bg-indigo-100', 'text-indigo-700'); | |
document.getElementById(`${tool}Tool`).classList.add('bg-indigo-100', 'text-indigo-700'); | |
// Change cursor based on tool | |
switch(tool) { | |
case 'select': | |
mainWaveformContainer.style.cursor = 'default'; | |
break; | |
case 'cut': | |
mainWaveformContainer.style.cursor = 'crosshair'; | |
break; | |
case 'trim': | |
mainWaveformContainer.style.cursor = 'col-resize'; | |
break; | |
case 'fade': | |
mainWaveformContainer.style.cursor = 'pointer'; | |
break; | |
} | |
} | |
// Toggle play/pause | |
function togglePlay() { | |
isPlaying = !isPlaying; | |
if (isPlaying) { | |
document.getElementById('playBtn').classList.remove('bg-gray-200'); | |
document.getElementById('playBtn').classList.add('bg-indigo-600'); | |
document.getElementById('pauseBtn').classList.remove('bg-indigo-600'); | |
document.getElementById('pauseBtn').classList.add('bg-gray-200'); | |
} else { | |
document.getElementById('playBtn').classList.remove('bg-indigo-600'); | |
document.getElementById('playBtn').classList.add('bg-gray-200'); | |
document.getElementById('pauseBtn').classList.remove('bg-gray-200'); | |
document.getElementById('pauseBtn').classList.add('bg-indigo-600'); | |
} | |
} | |
// Stop playback | |
function stopPlayback() { | |
isPlaying = false; | |
currentTime = 0; | |
updateTimeDisplay(); | |
document.getElementById('playBtn').classList.remove('bg-indigo-600'); | |
document.getElementById('playBtn').classList.add('bg-gray-200'); | |
document.getElementById('pauseBtn').classList.remove('bg-indigo-600'); | |
document.getElementById('pauseBtn').classList.add('bg-gray-200'); | |
} | |
// Toggle loop | |
function toggleLoop() { | |
const loopBtn = document.getElementById('loopBtn'); | |
loopBtn.classList.toggle('bg-indigo-100'); | |
loopBtn.classList.toggle('text-indigo-700'); | |
} | |
// Adjust zoom level | |
function adjustZoom(amount) { | |
zoomLevel = Math.max(50, Math.min(200, zoomLevel + amount)); | |
zoomLevelDisplay.textContent = `${zoomLevel}%`; | |
// In a real app, this would adjust the waveform display | |
createTimelineMarkers(); | |
} | |
// Fit to view | |
function fitToView() { | |
zoomLevel = 100; | |
zoomLevelDisplay.textContent = `${zoomLevel}%`; | |
// In a real app, this would adjust the waveform display | |
createTimelineMarkers(); | |
} | |
// Update playback speed | |
function updateSpeed() { | |
const speed = parseInt(speedControl.value); | |
speedValue.textContent = `${(speed / 100).toFixed(1)}x`; | |
} | |
// Handle waveform mouse down | |
function handleWaveformMouseDown(e) { | |
if (currentTool === 'select' || currentTool === 'cut') { | |
const rect = mainWaveformContainer.getBoundingClientRect(); | |
const x = e.clientX - rect.left; | |
const percentage = (x / rect.width) * 100; | |
selectionStart = percentage; | |
selectionEnd = percentage; | |
selectionArea.style.left = `${percentage}%`; | |
selectionArea.style.width = '0'; | |
selectionArea.classList.remove('hidden'); | |
hasSelection = true; | |
updateSelectionInfo(); | |
} | |
} | |
// Handle waveform mouse move | |
function handleWaveformMouseMove(e) { | |
if (hasSelection && (currentTool === 'select' || currentTool === 'cut')) { | |
const rect = mainWaveformContainer.getBoundingClientRect(); | |
const x = e.clientX - rect.left; | |
const percentage = (x / rect.width) * 100; | |
selectionEnd = percentage; | |
if (percentage > selectionStart) { | |
selectionArea.style.left = `${selectionStart}%`; | |
selectionArea.style.width = `${percentage - selectionStart}%`; | |
} else { | |
selectionArea.style.left = `${percentage}%`; | |
selectionArea.style.width = `${selectionStart - percentage}%`; | |
} | |
updateSelectionInfo(); | |
} | |
} | |
// Handle waveform mouse up | |
function handleWaveformMouseUp() { | |
if (currentTool === 'cut' && hasSelection) { | |
// In a real app, this would cut the audio at the selection points | |
console.log(`Cut audio from ${selectionStart}% to ${selectionEnd}%`); | |
} | |
hasSelection = false; | |
} | |
// Update selection info display | |
function updateSelectionInfo() { | |
const startTime = (selectionStart / 100) * totalDuration; | |
const endTime = (selectionEnd / 100) * totalDuration; | |
const duration = Math.abs(endTime - startTime); | |
selectionInfo.innerHTML = ` | |
Start: <strong>${formatTimeLong(startTime)}</strong><br> | |
End: <strong>${formatTimeLong(endTime)}</strong><br> | |
Duration: <strong>${formatTimeLong(duration)}</strong> | |
`; | |
} | |
// Clear selection | |
function clearSelection() { | |
selectionArea.classList.add('hidden'); | |
selectionInfo.textContent = 'No selection made'; | |
selectionStart = 0; | |
selectionEnd = 0; | |
} | |
// Trim selection | |
function trimSelection() { | |
if (selectionStart === 0 && selectionEnd === 0) return; | |
// In a real app, this would trim the audio to the selected region | |
console.log(`Trim audio to ${selectionStart}% - ${selectionEnd}%`); | |
alert(`Audio trimmed to selected region (${formatTimeLong((selectionStart / 100) * totalDuration)} - ${formatTimeLong((selectionEnd / 100) * totalDuration)})`); | |
clearSelection(); | |
} | |
// Delete selection | |
function deleteSelection() { | |
if (selectionStart === 0 && selectionEnd === 0) return; | |
// In a real app, this would delete the selected region | |
console.log(`Delete audio from ${selectionStart}% to ${selectionEnd}%`); | |
alert(`Deleted selected audio region (${formatTimeLong((selectionStart / 100) * totalDuration)} - ${formatTimeLong((selectionEnd / 100) * totalDuration)})`); | |
clearSelection(); | |
} | |
// Apply fade effect | |
function applyFade(type) { | |
if (selectionStart === 0 && selectionEnd === 0) return; | |
// In a real app, this would apply a fade to the selected region | |
console.log(`Apply ${type} fade to ${selectionStart}% - ${selectionEnd}%`); | |
alert(`Applied ${type} fade to selected region`); | |
clearSelection(); | |
} | |
// Add a new track | |
function addTrack(name) { | |
trackCount++; | |
const template = document.getElementById('trackItemTemplate'); | |
const clone = template.cloneNode(true); | |
clone.id = `track-${trackCount}`; | |
clone.classList.remove('hidden'); | |
const trackName = clone.querySelector('.track-name'); | |
trackName.textContent = name; | |
// Set up event listeners for the new track | |
clone.querySelector('.toggle-mute').addEventListener('click', function() { | |
this.classList.toggle('text-red-500'); | |
console.log(`Toggle mute for ${name}`); | |
}); | |
clone.querySelector('.toggle-solo').addEventListener('click', function() { | |
this.classList.toggle('text-indigo-600'); | |
console.log(`Toggle solo for ${name}`); | |
}); | |
clone.querySelector('.toggle-visibility').addEventListener('click', function() { | |
this.classList.toggle('text-gray-500'); | |
this.classList.toggle('text-indigo-600'); | |
console.log(`Toggle visibility for ${name}`); | |
}); | |
clone.querySelector('.delete-track').addEventListener('click', function() { | |
if (confirm(`Delete track "${name}"?`)) { | |
clone.remove(); | |
console.log(`Deleted track ${name}`); | |
} | |
}); | |
const volumeSlider = clone.querySelector('.volume-slider'); | |
const volumeLevel = clone.querySelector('.volume-level'); | |
const volumeDb = clone.querySelector('.volume-db'); | |
volumeSlider.addEventListener('input', function() { | |
const value = parseFloat(this.value); | |
const percentage = ((value + 30) / 36) * 100; | |
volumeLevel.style.width = `${percentage}%`; | |
volumeDb.textContent = `${value.toFixed(1)} dB`; | |
// In a real app, this would adjust the track volume | |
console.log(`Set volume for ${name} to ${value} dB`); | |
}); | |
// Add to tracks container | |
document.getElementById('tracksContainer').appendChild(clone); | |
} | |
// Add an audio clip to a track | |
function addAudioClip(trackName, clipName, startTime, duration) { | |
clipCount++; | |
const template = document.getElementById('audioClipTemplate'); | |
const clone = template.cloneNode(true); | |
clone.id = `clip-${clipCount}`; | |
clone.classList.remove('hidden'); | |
const nameElement = clone.querySelector('.audio-clip-name'); | |
nameElement.textContent = clipName; | |
// Position the clip based on start time and duration | |
const containerWidth = document.getElementById('audioClipsContainer').offsetWidth; | |
const totalSeconds = totalDuration; | |
const pixelsPerSecond = containerWidth / (totalSeconds * (zoomLevel / 100)); | |
clone.style.width = `${duration * pixelsPerSecond}px`; | |
clone.style.left = `${startTime * pixelsPerSecond}px`; | |
// Generate a mini waveform for the clip | |
const waveformElement = clone.querySelector('.audio-clip-waveform'); | |
generateMiniWaveform(waveformElement, duration * 10); | |
// Set up event listeners for the clip | |
clone.addEventListener('click', function() { | |
// Select this clip | |
document.querySelectorAll('#audioClipsContainer > div').forEach(c => { | |
c.classList.remove('border-indigo-500', 'bg-indigo-50'); | |
}); | |
this.classList.add('border-indigo-500', 'bg-indigo-50'); | |
console.log(`Selected clip: ${clipName}`); | |
}); | |
clone.querySelector('.delete-clip').addEventListener('click', function(e) { | |
e.stopPropagation(); | |
if (confirm(`Delete clip "${clipName}"?`)) { | |
clone.remove(); | |
console.log(`Deleted clip ${clipName}`); | |
} | |
}); | |
// Add to audio clips container | |
document.getElementById('audioClipsContainer').appendChild(clone); | |
} | |
// Generate a mini waveform for a clip | |
function generateMiniWaveform(element, barCount) { | |
element.innerHTML = ''; | |
for (let i = 0; i < barCount; i++) { | |
const bar = document.createElement('div'); | |
bar.className = 'inline-block h-full w-px bg-indigo-500'; | |
const height = 20 + Math.sin(i / 3) * 15 + Math.random() * 10; | |
bar.style.height = `${height}%`; | |
element.appendChild(bar); | |
} | |
} | |
// Add an effect | |
function addEffect() { | |
const effectName = prompt("Enter effect name:"); | |
if (!effectName) return; | |
const effectsList = document.getElementById('effectsList'); | |
const effectItem = document.createElement('div'); | |
effectItem.className = 'effect-card bg-gray-50 rounded-lg p-3 border border-gray-200 transition cursor-pointer'; | |
effectItem.innerHTML = ` | |
<div class="flex justify-between items-center"> | |
<div class="flex items-center space-x-3"> | |
<div class="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center text-purple-600"> | |
<i class="fas fa-sliders-h"></i> | |
</div> | |
<div> | |
<h3 class="font-medium">${effectName}</h3> | |
<p class="text-xs text-gray-500">Click to edit settings</p> | |
</div> | |
</div> | |
<button class="text-gray-500 hover:text-red-500 delete-effect"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="effect-panel mt-2" id="effect-panel-${effectName.toLowerCase().replace(' ', '-')}"> | |
<!-- Effect panel content will be added here --> | |
</div> | |
`; | |
// Set up event listeners | |
effectItem.querySelector('.delete-effect').addEventListener('click', function(e) { | |
e.stopPropagation(); | |
if (confirm(`Remove effect "${effectName}"?`)) { | |
effectItem.remove(); | |
console.log(`Removed effect ${effectName}`); | |
} | |
}); | |
effectItem.addEventListener('click', function() { | |
const panel = this.querySelector('.effect-panel'); | |
const template = document.getElementById('effectPanelTemplate'); | |
if (!panel.innerHTML.trim()) { | |
const panelClone = template.cloneNode(true); | |
panelClone.classList.remove('hidden'); | |
panelClone.classList.add('open'); | |
panelClone.id = ''; | |
// Update panel title | |
panelClone.querySelector('h4').textContent = `${effectName} Settings`; | |
// Set up close button | |
panelClone.querySelector('.close-effect-panel').addEventListener('click', function(e) { | |
e.stopPropagation(); | |
panelClone.classList.remove('open'); | |
}); | |
// Set up remove button | |
panelClone.querySelector('.remove-effect').addEventListener('click', function(e) { | |
e.stopPropagation(); | |
if (confirm(`Remove effect "${effectName}"?`)) { | |
effectItem.remove(); | |
console.log(`Removed effect ${effectName}`); | |
} | |
}); | |
panel.appendChild(panelClone); | |
} else { | |
panel.querySelector('.effect-panel').classList.toggle('open'); | |
} | |
}); | |
effectsList.appendChild(effectItem); | |
} | |
// Handle keyboard shortcuts | |
function handleKeyboardShortcuts(e) { | |
// Space to play/pause | |
if (e.code === 'Space') { | |
e.preventDefault(); | |
togglePlay(); | |
} | |
// Tool shortcuts | |
if (e.key === 'v') setActiveTool('select'); | |
if (e.key === 'c') setActiveTool('cut'); | |
if (e.key === 't') setActiveTool('trim'); | |
if (e.key === 'f') setActiveTool('fade'); | |
// Zoom shortcuts | |
if (e.ctrlKey && e.key === '+') adjustZoom(10); | |
if (e.ctrlKey && e.key === '-') adjustZoom(-10); | |
if (e.ctrlKey && e.key === '0') fitToView(); | |
// Playback control | |
if (e.key === 'l') toggleLoop(); | |
if (e.key === 's') stopPlayback(); | |
} | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=MrEzzat/audio-edititng" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |