C50BARZ's picture
Add 2 files
4d108ab verified
<!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">
<!-- Left Turntable -->
<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>
<!-- Right Turntable -->
<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>
<!-- Mixer Section -->
<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>
<!-- Visualizer -->
<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() {
// Audio context setup
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
// Master gain node
const masterGain = audioContext.createGain();
masterGain.gain.value = 0.8;
masterGain.connect(audioContext.destination);
// Deck A elements
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');
// Deck B elements
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');
// Mixer elements
const crossfader = document.getElementById('crossfader');
const masterPlay = document.getElementById('masterPlay');
// Visualizer
const visualizer = document.getElementById('visualizer');
const canvasCtx = visualizer.getContext('2d');
// Audio buffers and nodes
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;
// Initialize audio nodes for Deck A
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');
};
}
}
// Initialize audio nodes for Deck B
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');
};
}
}
// Load audio file for Deck A
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);
}
});
// Load audio file for Deck B
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);
}
});
// Play Deck A
playButtonA.addEventListener('click', function() {
if (audioBufferA && !isPlayingA) {
initDeckA();
sourceNodeA.start(0);
isPlayingA = true;
recordA.classList.add('record-spin');
// Adjust speed based on pitch
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');
}
}
});
// Play Deck B
playButtonB.addEventListener('click', function() {
if (audioBufferB && !isPlayingB) {
initDeckB();
sourceNodeB.start(0);
isPlayingB = true;
recordB.classList.add('record-spin');
// Adjust speed based on pitch
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');
}
}
});
// Stop Deck A
stopButtonA.addEventListener('click', function() {
if (isPlayingA) {
sourceNodeA.stop();
isPlayingA = false;
recordA.classList.remove('record-spin', 'fast', 'slow');
}
});
// Stop Deck B
stopButtonB.addEventListener('click', function() {
if (isPlayingB) {
sourceNodeB.stop();
isPlayingB = false;
recordB.classList.remove('record-spin', 'fast', 'slow');
}
});
// Cue Deck A
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);
// Briefly show spinning record
recordA.classList.add('record-spin', 'fast');
setTimeout(() => {
recordA.classList.remove('record-spin', 'fast');
}, 500);
}
});
// Cue Deck B
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);
// Briefly show spinning record
recordB.classList.add('record-spin', 'fast');
setTimeout(() => {
recordB.classList.remove('record-spin', 'fast');
}, 500);
}
});
// Pitch control for Deck A
pitchSliderA.addEventListener('input', function() {
playbackRateA = 1.0 + (this.value / 100);
if (isPlayingA) {
sourceNodeA.playbackRate.value = playbackRateA;
// Adjust visual spin speed
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');
}
}
});
// Pitch control for Deck B
pitchSliderB.addEventListener('input', function() {
playbackRateB = 1.0 + (this.value / 100);
if (isPlayingB) {
sourceNodeB.playbackRate.value = playbackRateB;
// Adjust visual spin speed
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');
}
}
});
// Volume control for Deck A
volumeSliderA.addEventListener('input', function() {
if (gainNodeA) {
gainNodeA.gain.value = this.value;
}
});
// Volume control for Deck B
volumeSliderB.addEventListener('input', function() {
if (gainNodeB) {
gainNodeB.gain.value = this.value;
}
});
// Master volume for Deck A
masterVolumeA.addEventListener('input', function() {
if (pannerNodeA) {
pannerNodeA.pan.value = -0.5 + (this.value * 0.5);
}
});
// Master volume for Deck B
masterVolumeB.addEventListener('input', function() {
if (pannerNodeB) {
pannerNodeB.pan.value = 0.5 - (this.value * 0.5);
}
});
// Crossfader control
crossfader.addEventListener('input', function() {
const value = parseFloat(this.value);
if (pannerNodeA && pannerNodeB) {
pannerNodeA.pan.value = -1 + value;
pannerNodeB.pan.value = 1 - value;
}
});
// Master play button
masterPlay.addEventListener('click', function() {
if (audioBufferA && !isPlayingA) {
playButtonA.click();
}
if (audioBufferB && !isPlayingB) {
playButtonB.click();
}
});
// Scratch functionality for Deck A
let isScratchingA = false;
let lastX = 0;
let scratchBufferA = null;
scratchAreaA.addEventListener('mousedown', function(e) {
if (audioBufferA) {
isScratchingA = true;
lastX = e.clientX;
// Create a new buffer source for scratching
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;
// Adjust playback rate based on mouse movement
const rate = 1.0 + (deltaX * 0.01);
scratchBufferA.playbackRate.value = rate;
// Adjust visual spin speed
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');
}
});
// Scratch functionality for Deck B
let isScratchingB = false;
let scratchBufferB = null;
scratchAreaB.addEventListener('mousedown', function(e) {
if (audioBufferB) {
isScratchingB = true;
lastX = e.clientX;
// Create a new buffer source for scratching
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;
// Adjust playback rate based on mouse movement
const rate = 1.0 + (deltaX * 0.01);
scratchBufferB.playbackRate.value = rate;
// Adjust visual spin speed
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');
}
});
// Visualizer setup
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;
// Create gradient
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();
// Handle tab visibility change to resume audio context
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>