|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Turntable DJ Scratch App</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> |
|
@keyframes spin { |
|
0% { transform: rotate(0deg); } |
|
100% { transform: rotate(360deg); } |
|
} |
|
|
|
.record-spin { |
|
animation: spin 2s linear infinite; |
|
} |
|
|
|
.record-spin.slow { |
|
animation-duration: 4s; |
|
} |
|
|
|
.record-spin.fast { |
|
animation-duration: 0.5s; |
|
} |
|
|
|
.crossfader { |
|
-webkit-appearance: none; |
|
width: 100%; |
|
height: 8px; |
|
background: #ddd; |
|
outline: none; |
|
border-radius: 4px; |
|
} |
|
|
|
.crossfader::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
appearance: none; |
|
width: 24px; |
|
height: 24px; |
|
border-radius: 50%; |
|
background: #4f46e5; |
|
cursor: pointer; |
|
} |
|
|
|
.scratch-area { |
|
touch-action: none; |
|
} |
|
|
|
.pitch-slider { |
|
-webkit-appearance: none; |
|
height: 8px; |
|
background: linear-gradient(to right, #ef4444, #f59e0b, #10b981); |
|
border-radius: 4px; |
|
outline: none; |
|
} |
|
|
|
.pitch-slider::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
appearance: none; |
|
width: 20px; |
|
height: 20px; |
|
border-radius: 50%; |
|
background: white; |
|
border: 2px solid #4f46e5; |
|
cursor: pointer; |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-900 text-white min-h-screen"> |
|
<div class="container mx-auto px-4 py-8"> |
|
<header class="text-center mb-8"> |
|
<h1 class="text-4xl font-bold bg-gradient-to-r from-purple-500 to-blue-500 bg-clip-text text-transparent">Turntable DJ Scratch App</h1> |
|
<p class="text-gray-400 mt-2">Load your MP3 and scratch like a pro!</p> |
|
</header> |
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> |
|
|
|
<div class="bg-gray-800 rounded-xl p-6 shadow-lg"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-xl font-semibold text-purple-400">Deck A</h2> |
|
<div class="flex space-x-2"> |
|
<button id="playA" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg"> |
|
<i class="fas fa-play"></i> |
|
</button> |
|
<button id="stopA" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg"> |
|
<i class="fas fa-stop"></i> |
|
</button> |
|
<button id="cueA" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg"> |
|
<i class="fas fa-undo"></i> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="relative h-64 w-64 mx-auto mb-6"> |
|
<div class="absolute inset-0 bg-gray-900 rounded-full flex items-center justify-center"> |
|
<div id="recordA" class="relative h-56 w-56 rounded-full border-4 border-gray-700 bg-gradient-to-br from-gray-800 to-gray-900 flex items-center justify-center"> |
|
<div class="absolute h-6 w-6 rounded-full bg-gray-700 z-10"></div> |
|
<div class="absolute h-4 w-4 rounded-full bg-gray-600 z-10"></div> |
|
<div class="absolute h-12 w-12 rounded-full bg-gray-900 z-0"></div> |
|
</div> |
|
</div> |
|
<div class="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-8 h-16 bg-gray-700 rounded-b-lg"></div> |
|
</div> |
|
|
|
<div class="mb-4"> |
|
<label class="block text-sm font-medium text-gray-400 mb-2">Pitch</label> |
|
<input type="range" id="pitchA" class="pitch-slider w-full" min="-50" max="50" value="0"> |
|
</div> |
|
|
|
<div class="mb-4"> |
|
<label class="block text-sm font-medium text-gray-400 mb-2">Volume</label> |
|
<input type="range" id="volumeA" class="w-full" min="0" max="1" step="0.01" value="0.8"> |
|
</div> |
|
|
|
<div class="mb-4"> |
|
<label class="block text-sm font-medium text-gray-400 mb-2">Load MP3</label> |
|
<input type="file" id="fileA" accept="audio/mp3" class="block w-full text-sm text-gray-400 |
|
file:mr-4 file:py-2 file:px-4 |
|
file:rounded-md file:border-0 |
|
file:text-sm file:font-semibold |
|
file:bg-purple-600 file:text-white |
|
hover:file:bg-purple-700"> |
|
</div> |
|
|
|
<div id="scratchA" class="scratch-area h-16 bg-gray-700 rounded-lg flex items-center justify-center cursor-grab active:cursor-grabbing"> |
|
<span class="text-gray-400">Scratch Area (Drag with mouse)</span> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-800 rounded-xl p-6 shadow-lg"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-xl font-semibold text-blue-400">Deck B</h2> |
|
<div class="flex space-x-2"> |
|
<button id="playB" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg"> |
|
<i class="fas fa-play"></i> |
|
</button> |
|
<button id="stopB" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg"> |
|
<i class="fas fa-stop"></i> |
|
</button> |
|
<button id="cueB" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg"> |
|
<i class="fas fa-undo"></i> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="relative h-64 w-64 mx-auto mb-6"> |
|
<div class="absolute inset-0 bg-gray-900 rounded-full flex items-center justify-center"> |
|
<div id="recordB" class="relative h-56 w-56 rounded-full border-4 border-gray-700 bg-gradient-to-br from-gray-800 to-gray-900 flex items-center justify-center"> |
|
<div class="absolute h-6 w-6 rounded-full bg-gray-700 z-10"></div> |
|
<div class="absolute h-4 w-4 rounded-full bg-gray-600 z-10"></div> |
|
<div class="absolute h-12 w-12 rounded-full bg-gray-900 z-0"></div> |
|
</div> |
|
</div> |
|
<div class="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-8 h-16 bg-gray-700 rounded-b-lg"></div> |
|
</div> |
|
|
|
<div class="mb-4"> |
|
<label class="block text-sm font-medium text-gray-400 mb-2">Pitch</label> |
|
<input type="range" id="pitchB" class="pitch-slider w-full" min="-50" max="50" value="0"> |
|
</div> |
|
|
|
<div class="mb-4"> |
|
<label class="block text-sm font-medium text-gray-400 mb-2">Volume</label> |
|
<input type="range" id="volumeB" class="w-full" min="0" max="1" step="0.01" value="0.8"> |
|
</div> |
|
|
|
<div class="mb-4"> |
|
<label class="block text-sm font-medium text-gray-400 mb-2">Load MP3</label> |
|
<input type="file" id="fileB" accept="audio/mp3" class="block w-full text-sm text-gray-400 |
|
file:mr-4 file:py-2 file:px-4 |
|
file:rounded-md file:border-0 |
|
file:text-sm file:font-semibold |
|
file:bg-blue-600 file:text-white |
|
hover:file:bg-blue-700"> |
|
</div> |
|
|
|
<div id="scratchB" class="scratch-area h-16 bg-gray-700 rounded-lg flex items-center justify-center cursor-grab active:cursor-grabbing"> |
|
<span class="text-gray-400">Scratch Area (Drag with mouse)</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="mt-8 bg-gray-800 rounded-xl p-6 shadow-lg"> |
|
<h2 class="text-xl font-semibold text-center mb-6 text-indigo-400">Mixer Controls</h2> |
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> |
|
<div class="flex flex-col items-center"> |
|
<label class="block text-sm font-medium text-gray-400 mb-2">Deck A Volume</label> |
|
<input type="range" id="masterVolumeA" class="w-full h-32 -rotate-90" min="0" max="1" step="0.01" value="0.8"> |
|
</div> |
|
|
|
<div class="flex flex-col items-center justify-center"> |
|
<label class="block text-sm font-medium text-gray-400 mb-2">Crossfader</label> |
|
<input type="range" id="crossfader" class="crossfader w-full" min="0" max="1" step="0.01" value="0.5"> |
|
<div class="flex justify-between w-full mt-2 text-xs text-gray-400"> |
|
<span>A</span> |
|
<span>B</span> |
|
</div> |
|
</div> |
|
|
|
<div class="flex flex-col items-center"> |
|
<label class="block text-sm font-medium text-gray-400 mb-2">Deck B Volume</label> |
|
<input type="range" id="masterVolumeB" class="w-full h-32 rotate-90" min="0" max="1" step="0.01" value="0.8"> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-6 flex justify-center"> |
|
<button id="masterPlay" class="bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg text-lg font-semibold flex items-center"> |
|
<i class="fas fa-play mr-2"></i> Master Play |
|
</button> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="mt-8 bg-gray-800 rounded-xl p-6 shadow-lg"> |
|
<h2 class="text-xl font-semibold text-center mb-4 text-pink-400">Audio Visualizer</h2> |
|
<canvas id="visualizer" class="w-full h-32 bg-gray-900 rounded-lg"></canvas> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext; |
|
const audioContext = new AudioContext(); |
|
|
|
|
|
const masterGain = audioContext.createGain(); |
|
masterGain.gain.value = 0.8; |
|
masterGain.connect(audioContext.destination); |
|
|
|
|
|
const fileInputA = document.getElementById('fileA'); |
|
const playButtonA = document.getElementById('playA'); |
|
const stopButtonA = document.getElementById('stopA'); |
|
const cueButtonA = document.getElementById('cueA'); |
|
const pitchSliderA = document.getElementById('pitchA'); |
|
const volumeSliderA = document.getElementById('volumeA'); |
|
const masterVolumeA = document.getElementById('masterVolumeA'); |
|
const recordA = document.getElementById('recordA'); |
|
const scratchAreaA = document.getElementById('scratchA'); |
|
|
|
|
|
const fileInputB = document.getElementById('fileB'); |
|
const playButtonB = document.getElementById('playB'); |
|
const stopButtonB = document.getElementById('stopB'); |
|
const cueButtonB = document.getElementById('cueB'); |
|
const pitchSliderB = document.getElementById('pitchB'); |
|
const volumeSliderB = document.getElementById('volumeB'); |
|
const masterVolumeB = document.getElementById('masterVolumeB'); |
|
const recordB = document.getElementById('recordB'); |
|
const scratchAreaB = document.getElementById('scratchB'); |
|
|
|
|
|
const crossfader = document.getElementById('crossfader'); |
|
const masterPlay = document.getElementById('masterPlay'); |
|
|
|
|
|
const visualizer = document.getElementById('visualizer'); |
|
const canvasCtx = visualizer.getContext('2d'); |
|
|
|
|
|
let audioBufferA = null; |
|
let audioBufferB = null; |
|
let sourceNodeA = null; |
|
let sourceNodeB = null; |
|
let gainNodeA = null; |
|
let gainNodeB = null; |
|
let pannerNodeA = null; |
|
let pannerNodeB = null; |
|
let playbackRateA = 1.0; |
|
let playbackRateB = 1.0; |
|
let isPlayingA = false; |
|
let isPlayingB = false; |
|
|
|
|
|
function initDeckA() { |
|
if (audioBufferA) { |
|
sourceNodeA = audioContext.createBufferSource(); |
|
sourceNodeA.buffer = audioBufferA; |
|
|
|
gainNodeA = audioContext.createGain(); |
|
gainNodeA.gain.value = volumeSliderA.value; |
|
|
|
pannerNodeA = audioContext.createStereoPanner(); |
|
pannerNodeA.pan.value = -0.5; |
|
|
|
sourceNodeA.connect(gainNodeA); |
|
gainNodeA.connect(pannerNodeA); |
|
pannerNodeA.connect(masterGain); |
|
|
|
sourceNodeA.playbackRate.value = playbackRateA; |
|
sourceNodeA.loop = true; |
|
|
|
sourceNodeA.onended = function() { |
|
isPlayingA = false; |
|
recordA.classList.remove('record-spin'); |
|
}; |
|
} |
|
} |
|
|
|
|
|
function initDeckB() { |
|
if (audioBufferB) { |
|
sourceNodeB = audioContext.createBufferSource(); |
|
sourceNodeB.buffer = audioBufferB; |
|
|
|
gainNodeB = audioContext.createGain(); |
|
gainNodeB.gain.value = volumeSliderB.value; |
|
|
|
pannerNodeB = audioContext.createStereoPanner(); |
|
pannerNodeB.pan.value = 0.5; |
|
|
|
sourceNodeB.connect(gainNodeB); |
|
gainNodeB.connect(pannerNodeB); |
|
pannerNodeB.connect(masterGain); |
|
|
|
sourceNodeB.playbackRate.value = playbackRateB; |
|
sourceNodeB.loop = true; |
|
|
|
sourceNodeB.onended = function() { |
|
isPlayingB = false; |
|
recordB.classList.remove('record-spin'); |
|
}; |
|
} |
|
} |
|
|
|
|
|
fileInputA.addEventListener('change', function(e) { |
|
const file = e.target.files[0]; |
|
if (file) { |
|
const reader = new FileReader(); |
|
reader.onload = function(e) { |
|
audioContext.decodeAudioData(e.target.result) |
|
.then(buffer => { |
|
audioBufferA = buffer; |
|
initDeckA(); |
|
recordA.classList.add('record-spin'); |
|
recordA.classList.add('slow'); |
|
}) |
|
.catch(err => { |
|
console.error("Error decoding audio data", err); |
|
}); |
|
}; |
|
reader.readAsArrayBuffer(file); |
|
} |
|
}); |
|
|
|
|
|
fileInputB.addEventListener('change', function(e) { |
|
const file = e.target.files[0]; |
|
if (file) { |
|
const reader = new FileReader(); |
|
reader.onload = function(e) { |
|
audioContext.decodeAudioData(e.target.result) |
|
.then(buffer => { |
|
audioBufferB = buffer; |
|
initDeckB(); |
|
recordB.classList.add('record-spin'); |
|
recordB.classList.add('slow'); |
|
}) |
|
.catch(err => { |
|
console.error("Error decoding audio data", err); |
|
}); |
|
}; |
|
reader.readAsArrayBuffer(file); |
|
} |
|
}); |
|
|
|
|
|
playButtonA.addEventListener('click', function() { |
|
if (audioBufferA && !isPlayingA) { |
|
initDeckA(); |
|
sourceNodeA.start(0); |
|
isPlayingA = true; |
|
recordA.classList.add('record-spin'); |
|
|
|
|
|
if (pitchSliderA.value > 0) { |
|
recordA.classList.remove('slow'); |
|
recordA.classList.add('fast'); |
|
} else if (pitchSliderA.value < 0) { |
|
recordA.classList.remove('fast'); |
|
recordA.classList.add('slow'); |
|
} else { |
|
recordA.classList.remove('fast', 'slow'); |
|
} |
|
} |
|
}); |
|
|
|
|
|
playButtonB.addEventListener('click', function() { |
|
if (audioBufferB && !isPlayingB) { |
|
initDeckB(); |
|
sourceNodeB.start(0); |
|
isPlayingB = true; |
|
recordB.classList.add('record-spin'); |
|
|
|
|
|
if (pitchSliderB.value > 0) { |
|
recordB.classList.remove('slow'); |
|
recordB.classList.add('fast'); |
|
} else if (pitchSliderB.value < 0) { |
|
recordB.classList.remove('fast'); |
|
recordB.classList.add('slow'); |
|
} else { |
|
recordB.classList.remove('fast', 'slow'); |
|
} |
|
} |
|
}); |
|
|
|
|
|
stopButtonA.addEventListener('click', function() { |
|
if (isPlayingA) { |
|
sourceNodeA.stop(); |
|
isPlayingA = false; |
|
recordA.classList.remove('record-spin', 'fast', 'slow'); |
|
} |
|
}); |
|
|
|
|
|
stopButtonB.addEventListener('click', function() { |
|
if (isPlayingB) { |
|
sourceNodeB.stop(); |
|
isPlayingB = false; |
|
recordB.classList.remove('record-spin', 'fast', 'slow'); |
|
} |
|
}); |
|
|
|
|
|
cueButtonA.addEventListener('click', function() { |
|
if (audioBufferA) { |
|
if (isPlayingA) { |
|
sourceNodeA.stop(); |
|
isPlayingA = false; |
|
recordA.classList.remove('record-spin', 'fast', 'slow'); |
|
} |
|
|
|
initDeckA(); |
|
sourceNodeA.start(0); |
|
setTimeout(() => { |
|
sourceNodeA.stop(); |
|
}, 500); |
|
|
|
|
|
recordA.classList.add('record-spin', 'fast'); |
|
setTimeout(() => { |
|
recordA.classList.remove('record-spin', 'fast'); |
|
}, 500); |
|
} |
|
}); |
|
|
|
|
|
cueButtonB.addEventListener('click', function() { |
|
if (audioBufferB) { |
|
if (isPlayingB) { |
|
sourceNodeB.stop(); |
|
isPlayingB = false; |
|
recordB.classList.remove('record-spin', 'fast', 'slow'); |
|
} |
|
|
|
initDeckB(); |
|
sourceNodeB.start(0); |
|
setTimeout(() => { |
|
sourceNodeB.stop(); |
|
}, 500); |
|
|
|
|
|
recordB.classList.add('record-spin', 'fast'); |
|
setTimeout(() => { |
|
recordB.classList.remove('record-spin', 'fast'); |
|
}, 500); |
|
} |
|
}); |
|
|
|
|
|
pitchSliderA.addEventListener('input', function() { |
|
playbackRateA = 1.0 + (this.value / 100); |
|
if (isPlayingA) { |
|
sourceNodeA.playbackRate.value = playbackRateA; |
|
|
|
|
|
if (this.value > 0) { |
|
recordA.classList.remove('slow'); |
|
recordA.classList.add('fast'); |
|
} else if (this.value < 0) { |
|
recordA.classList.remove('fast'); |
|
recordA.classList.add('slow'); |
|
} else { |
|
recordA.classList.remove('fast', 'slow'); |
|
} |
|
} |
|
}); |
|
|
|
|
|
pitchSliderB.addEventListener('input', function() { |
|
playbackRateB = 1.0 + (this.value / 100); |
|
if (isPlayingB) { |
|
sourceNodeB.playbackRate.value = playbackRateB; |
|
|
|
|
|
if (this.value > 0) { |
|
recordB.classList.remove('slow'); |
|
recordB.classList.add('fast'); |
|
} else if (this.value < 0) { |
|
recordB.classList.remove('fast'); |
|
recordB.classList.add('slow'); |
|
} else { |
|
recordB.classList.remove('fast', 'slow'); |
|
} |
|
} |
|
}); |
|
|
|
|
|
volumeSliderA.addEventListener('input', function() { |
|
if (gainNodeA) { |
|
gainNodeA.gain.value = this.value; |
|
} |
|
}); |
|
|
|
|
|
volumeSliderB.addEventListener('input', function() { |
|
if (gainNodeB) { |
|
gainNodeB.gain.value = this.value; |
|
} |
|
}); |
|
|
|
|
|
masterVolumeA.addEventListener('input', function() { |
|
if (pannerNodeA) { |
|
pannerNodeA.pan.value = -0.5 + (this.value * 0.5); |
|
} |
|
}); |
|
|
|
|
|
masterVolumeB.addEventListener('input', function() { |
|
if (pannerNodeB) { |
|
pannerNodeB.pan.value = 0.5 - (this.value * 0.5); |
|
} |
|
}); |
|
|
|
|
|
crossfader.addEventListener('input', function() { |
|
const value = parseFloat(this.value); |
|
if (pannerNodeA && pannerNodeB) { |
|
pannerNodeA.pan.value = -1 + value; |
|
pannerNodeB.pan.value = 1 - value; |
|
} |
|
}); |
|
|
|
|
|
masterPlay.addEventListener('click', function() { |
|
if (audioBufferA && !isPlayingA) { |
|
playButtonA.click(); |
|
} |
|
if (audioBufferB && !isPlayingB) { |
|
playButtonB.click(); |
|
} |
|
}); |
|
|
|
|
|
let isScratchingA = false; |
|
let lastX = 0; |
|
let scratchBufferA = null; |
|
|
|
scratchAreaA.addEventListener('mousedown', function(e) { |
|
if (audioBufferA) { |
|
isScratchingA = true; |
|
lastX = e.clientX; |
|
|
|
|
|
if (isPlayingA) { |
|
sourceNodeA.stop(); |
|
isPlayingA = false; |
|
} |
|
|
|
scratchBufferA = audioContext.createBufferSource(); |
|
scratchBufferA.buffer = audioBufferA; |
|
scratchBufferA.connect(gainNodeA); |
|
scratchBufferA.loop = true; |
|
scratchBufferA.start(0); |
|
|
|
recordA.classList.add('record-spin'); |
|
recordA.classList.remove('fast', 'slow'); |
|
} |
|
}); |
|
|
|
document.addEventListener('mousemove', function(e) { |
|
if (isScratchingA && scratchBufferA) { |
|
const deltaX = e.clientX - lastX; |
|
lastX = e.clientX; |
|
|
|
|
|
const rate = 1.0 + (deltaX * 0.01); |
|
scratchBufferA.playbackRate.value = rate; |
|
|
|
|
|
if (deltaX > 0) { |
|
recordA.classList.remove('slow'); |
|
recordA.classList.add('fast'); |
|
} else if (deltaX < 0) { |
|
recordA.classList.remove('fast'); |
|
recordA.classList.add('slow'); |
|
} |
|
} |
|
}); |
|
|
|
document.addEventListener('mouseup', function() { |
|
if (isScratchingA) { |
|
isScratchingA = false; |
|
|
|
if (scratchBufferA) { |
|
scratchBufferA.stop(); |
|
scratchBufferA = null; |
|
} |
|
|
|
recordA.classList.remove('record-spin', 'fast', 'slow'); |
|
} |
|
}); |
|
|
|
|
|
let isScratchingB = false; |
|
let scratchBufferB = null; |
|
|
|
scratchAreaB.addEventListener('mousedown', function(e) { |
|
if (audioBufferB) { |
|
isScratchingB = true; |
|
lastX = e.clientX; |
|
|
|
|
|
if (isPlayingB) { |
|
sourceNodeB.stop(); |
|
isPlayingB = false; |
|
} |
|
|
|
scratchBufferB = audioContext.createBufferSource(); |
|
scratchBufferB.buffer = audioBufferB; |
|
scratchBufferB.connect(gainNodeB); |
|
scratchBufferB.loop = true; |
|
scratchBufferB.start(0); |
|
|
|
recordB.classList.add('record-spin'); |
|
recordB.classList.remove('fast', 'slow'); |
|
} |
|
}); |
|
|
|
document.addEventListener('mousemove', function(e) { |
|
if (isScratchingB && scratchBufferB) { |
|
const deltaX = e.clientX - lastX; |
|
lastX = e.clientX; |
|
|
|
|
|
const rate = 1.0 + (deltaX * 0.01); |
|
scratchBufferB.playbackRate.value = rate; |
|
|
|
|
|
if (deltaX > 0) { |
|
recordB.classList.remove('slow'); |
|
recordB.classList.add('fast'); |
|
} else if (deltaX < 0) { |
|
recordB.classList.remove('fast'); |
|
recordB.classList.add('slow'); |
|
} |
|
} |
|
}); |
|
|
|
document.addEventListener('mouseup', function() { |
|
if (isScratchingB) { |
|
isScratchingB = false; |
|
|
|
if (scratchBufferB) { |
|
scratchBufferB.stop(); |
|
scratchBufferB = null; |
|
} |
|
|
|
recordB.classList.remove('record-spin', 'fast', 'slow'); |
|
} |
|
}); |
|
|
|
|
|
let analyser = audioContext.createAnalyser(); |
|
analyser.fftSize = 256; |
|
masterGain.connect(analyser); |
|
|
|
const bufferLength = analyser.frequencyBinCount; |
|
const dataArray = new Uint8Array(bufferLength); |
|
|
|
visualizer.width = visualizer.offsetWidth; |
|
visualizer.height = visualizer.offsetHeight; |
|
|
|
function drawVisualizer() { |
|
requestAnimationFrame(drawVisualizer); |
|
|
|
analyser.getByteFrequencyData(dataArray); |
|
|
|
canvasCtx.fillStyle = 'rgb(17, 24, 39)'; |
|
canvasCtx.fillRect(0, 0, visualizer.width, visualizer.height); |
|
|
|
const barWidth = (visualizer.width / bufferLength) * 2.5; |
|
let x = 0; |
|
|
|
for (let i = 0; i < bufferLength; i++) { |
|
const barHeight = (dataArray[i] / 255) * visualizer.height; |
|
|
|
|
|
const gradient = canvasCtx.createLinearGradient(0, 0, 0, visualizer.height); |
|
gradient.addColorStop(0, '#4f46e5'); |
|
gradient.addColorStop(0.5, '#8b5cf6'); |
|
gradient.addColorStop(1, '#ec4899'); |
|
|
|
canvasCtx.fillStyle = gradient; |
|
canvasCtx.fillRect(x, visualizer.height - barHeight, barWidth, barHeight); |
|
|
|
x += barWidth + 1; |
|
} |
|
} |
|
|
|
drawVisualizer(); |
|
|
|
|
|
document.addEventListener('visibilitychange', function() { |
|
if (document.visibilityState === 'visible') { |
|
audioContext.resume(); |
|
} |
|
}); |
|
}); |
|
</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=C50BARZ/turntable-dj-scratch-app" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |